Academy → Developer GuideOfficial documentation · Arabic guidance

Extending the CLI

توسيع سطر الأوامر

Advanced4 min readLesson 122 questions✓ 2026-08-18
Before you read

What this page is, and what it holds.

This page covers Extending the CLI. You will use hermes source here; about 4 minutes to read. A plugin runs with the agent's full permissions. Do not install one you cannot read.

5sections
8code examples
1tables
1commands
699source words
The official one-line description

Build wrapper CLIs that extend the Hermes TUI with custom widgets, keybindings, and layout changes

What you will be able to do

Outcomes taken from this page, not a template.

  • Understand what الإضافات is and when you need it.
  • Run hermes source and understand what happens next.
  • Read the table and take only the row that applies to you.
Identifiers you will meet

Exactly as they appear in Hermes.

Commands
  • hermes source
Page map

Jump to the part you need.

  1. 01Extension points
  2. 02Quick start: a wrapper CLI
  3. 03Hook reference
  4. 04Layout diagram
  5. 05Tips
The full official page

Nothing summarised away.

The documentation body below is reproduced from the official source so commands and identifiers stay exact. Each section carries a short note describing what it contains.

Hermes exposes protected extension hooks on HermesCLI so wrapper CLIs can add widgets, keybindings, and layout customizations without overriding the 1000+ line run() method. This keeps your extension decoupled from internal changes.

Extension points

A lookup table. Do not read it all; find the row that applies to you.

There are five extension seams available:

HookPurposeOverride when...
_get_extra_tui_widgets()Inject widgets into the layoutYou need a persistent UI element (panel, status line, mini-player)
_register_extra_tui_keybindings(kb, *, input_area)Add keyboard shortcutsYou need hotkeys (toggle panels, transport controls, modal shortcuts)
_build_tui_layout_children(**widgets)Full control over widget orderingYou need to reorder or wrap existing widgets (rare)
process_command()Add custom slash commandsYou need /mycommand handling (pre-existing hook)
_build_tui_style_dict()Custom prompt_toolkit stylesYou need custom colors or styling (pre-existing hook)

The first three are new protected hooks. The last two already existed.

Quick start: a wrapper CLI

Ordered, practical steps. Run one and confirm it worked before moving on. Commands here: hermes source.

Python46 lines
#!/usr/bin/env python3
"""my_cli.py — Example wrapper CLI that extends Hermes."""

from cli import HermesCLI
from prompt_toolkit.layout import FormattedTextControl, Window
from prompt_toolkit.filters import Condition


class MyCLI(HermesCLI):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._panel_visible = False

    def _get_extra_tui_widgets(self):
        """Add a toggleable info panel above the status bar."""
        cli_ref = self
        return [
            Window(
                FormattedTextControl(lambda: "📊 My custom panel content"),
                height=1,
                filter=Condition(lambda: cli_ref._panel_visible),
            ),
        ]

    def _register_extra_tui_keybindings(self, kb, *, input_area):
        """F2 toggles the custom panel."""
        cli_ref = self

        @kb.add("f2")
        def _toggle_panel(event):
            cli_ref._panel_visible = not cli_ref._panel_visible

    def process_command(self, cmd: str) -> bool:
        """Add a /panel slash command."""
        if cmd.strip().lower() == "/panel":
            self._panel_visible = not self._panel_visible
            state = "visible" if self._panel_visible else "hidden"
            print(f"Panel is now {state}")
            return True
        return super().process_command(cmd)


if __name__ == "__main__":
    cli = MyCLI()
    cli.run()

Run it:

Shell3 lines
cd ~/.hermes/hermes-agent
source .venv/bin/activate
python my_cli.py

Hook reference

Explains the idea itself. Read it slowly; the later sections build on it.

getextratuiwidgets()

Returns a list of prompt_toolkit widgets to insert into the TUI layout. Widgets appear between the spacer and the status bar — above the input area but below the main output.

Python2 lines
def _get_extra_tui_widgets(self) -> list:
    return []  # default: no extra widgets

Each widget should be a prompt_toolkit container (e.g., Window, ConditionalContainer, HSplit). Use ConditionalContainer or filter=Condition(...) to make widgets toggleable.

Python10 lines
from prompt_toolkit.layout import ConditionalContainer, Window, FormattedTextControl
from prompt_toolkit.filters import Condition

def _get_extra_tui_widgets(self):
    return [
        ConditionalContainer(
            Window(FormattedTextControl("Status: connected"), height=1),
            filter=Condition(lambda: self._show_status),
        ),
    ]

registerextratuikeybindings(kb, , inputarea)

Called after Hermes registers its own keybindings and before the layout is built. Add your keybindings to kb.

Python2 lines
def _register_extra_tui_keybindings(self, kb, *, input_area):
    pass  # default: no extra keybindings

Parameters:

  • kb — The KeyBindings instance for the prompt_toolkit application
  • input_area — The main TextArea widget, if you need to read or manipulate user input
Python10 lines
def _register_extra_tui_keybindings(self, kb, *, input_area):
    cli_ref = self

    @kb.add("f3")
    def _clear_input(event):
        input_area.text = ""

    @kb.add("f4")
    def _insert_template(event):
        input_area.text = "/search "

Avoid conflicts with built-in keybindings: Enter (submit), Escape Enter (newline), Ctrl-C (interrupt), Ctrl-D (exit), Tab (auto-suggest accept). Function keys F2+ and Ctrl-combinations are generally safe.

buildtuilayoutchildren(widgets)

Override this only when you need full control over widget ordering. Most extensions should use _get_extra_tui_widgets() instead.

Python5 lines
def _build_tui_layout_children(self, *, sudo_widget, secret_widget,
    approval_widget, clarify_widget, model_picker_widget=None,
    spinner_widget=None, spacer, status_bar, input_rule_top,
    image_bar, input_area, input_rule_bot, voice_status_bar,
    completions_menu) -> list:

The default implementation returns (any None widgets are filtered out):

Python18 lines
[
    Window(height=0),       # anchor
    sudo_widget,            # sudo password prompt (conditional)
    secret_widget,          # secret input prompt (conditional)
    approval_widget,        # dangerous command approval (conditional)
    clarify_widget,         # clarify question UI (conditional)
    model_picker_widget,    # model picker overlay (conditional)
    spinner_widget,         # thinking spinner (conditional)
    spacer,                 # fills remaining vertical space
    *self._get_extra_tui_widgets(),  # YOUR WIDGETS GO HERE
    status_bar,             # model/token/context status line
    input_rule_top,         # ─── border above input
    image_bar,              # attached images indicator
    input_area,             # user text input
    input_rule_bot,         # ─── border below input
    voice_status_bar,       # voice mode status (conditional)
    completions_menu,       # autocomplete dropdown
]

Layout diagram

Explains the idea itself. Read it slowly; the later sections build on it.

The default layout from top to bottom:

  1. Output area — scrolling conversation history
  2. Spacer
  3. Extra widgets — from _get_extra_tui_widgets()
  4. Status bar — model, context %, elapsed time
  5. Image bar — attached image count
  6. Input area — user prompt
  7. Voice status — recording indicator
  8. Completions menu — autocomplete suggestions

Tips

Explains the idea itself. Read it slowly; the later sections build on it.

  • Invalidate the display after state changes: call self._invalidate() to trigger a prompt_toolkit redraw.
  • Access agent state: self.agent, self.model, self.conversation_history are all available.
  • Custom styles: Override _build_tui_style_dict() and add entries for your custom style classes.
  • Slash commands: Override process_command(), handle your commands, and call super().process_command(cmd) for everything else.
  • Don't override run() unless absolutely necessary — the extension hooks exist specifically to avoid that coupling.
Knowledge check

2 questions answered by this page alone.

Every option is a real identifier from the Hermes documentation. The wrong ones are real too, just from other pages.

1. In this lesson's table, what is the “Purpose” for “getextratuiwidgets()”?
2. Which of these headings does not appear in this lesson?