# Hermes CLI Development

> Hermes CLI Development

- Skill: `lucadominguez/hermes-cli-development` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lucadominguez/hermes-cli-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lucadominguez/hermes-cli-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lucadominguez (https://skillmd.com/u/lucadominguez)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lucadominguez/hermes-cli-development

---

# Hermes CLI Development

Techniques for modifying and extending the Hermes Agent CLI source code
(`~/.hermes/hermes-agent/cli.py` and related files). Use this skill when
adding new features, integrating with the terminal, or understanding how
the CLI session lifecycle works.

## Quick Reference: Key Files

| File | What it does |
|------|-------------|
| `cli.py` | `HermesCLI` class — interactive CLI, session management, slash commands |
| `hermes_state.py` | `SessionDB` — SQLite session store, title management, FTS5 search |
| `run_agent.py` | `AIAgent` — core conversation loop |
| `hermes_cli/commands.py` | Central slash command registry |
| `agent/prompt_builder.py` | System prompt assembly, environment hints |
| `tui_gateway/server.py` | Gateway RPC — `session.active_list`, live session state for TUI |
| `ui-tui/src/app/useMainApp.ts` | TUI terminal title via `useTerminalTitle()` + `composeTabTitle()` |
| `ui-tui/src/domain/paths.ts` | `composeTabTitle()` — builds terminal titlebar string |

**References:** `references/terminal-title-patch.md` — full patch history, re-application notes, and TUI/desktop title path documentation.

## Session Title Flow

Hermes stores session titles **only in state.db** via `SessionDB.set_session_title()`.
There is no auto-generation — the `auxiliary.title_generation` config key exists
in `config.yaml` but is **not wired up** in the codebase. Titles only exist when:

1. User runs `/title <name>` (stored immediately or deferred via `_pending_title`)
2. Branch sessions auto-derive titles from parent
3. Resume preserves existing titles

Key methods:
- `self._session_db.get_session_title(session_id)` → `str | None`
- `self._session_db.set_session_title(session_id, title)` → `bool`
- `self._pending_title` — deferred title applied when agent initializes
- `self.conversation_history` — list of `{role, content}` dicts, available for fallback extraction

## Terminal Title Integration

Hermes does NOT set the terminal window title by default. The terminal emulator
(Windows Terminal, iTerm2, GNOME Terminal) shows whatever the shell/process
provides, which on WSL+Windows Terminal shows "hermes default" (process name + profile).

### OSC Escape Sequence Pattern

```python
def _set_terminal_title(self, title: str = None):
    """Set terminal window/tab title via OSC escape sequences."""
    try:
        if title is None:
            # Fallback chain: DB title → first user message → session ID
            if self._session_db:
                db_title = self._session_db.get_session_title(self.session_id)
                if db_title:
                    title = db_title
        if title is None:
            # Extract from first user message in conversation_history
            for msg in (self.conversation_history or []):
                if msg.get("role") == "user":
                    raw = msg.get("content", "")
                    if isinstance(raw, str) and raw.strip():
                        first_line = raw.strip().split('\n')[0]
                        if not first_line.startswith('/'):
                            title = first_line[:60].strip()
                            if len(raw.strip()) > 60:
                                title += "…"
                            break
        if title is None:
            title = self.session_id[:12] if self.session_id else "hermes"

        # OSC 0 = icon name + window title, OSC 2 = window title only
        sys.stdout.write(f"\033]0;Hermes: {title}\007")
        sys.stdout.write(f"\033]2;Hermes: {title}\007")
        sys.stdout.flush()
    except Exception:
        pass  # Never let title setting break the CLI
```

Both `\033]0;` and `\033]2;` are sent for max compatibility:
- `\033]0;` — sets both icon name and window title (wider support)
- `\033]2;` — sets just the window title (preferred by modern terminals)
- Works on: Windows Terminal, iTerm2, GNOME Terminal, Konsole, Alacritty, Kitty

### Where to Hook In (Lifecycle Points)

When adding a feature like terminal titles, wire it up at these key points in `HermesCLI`:

| Method | Line (approx) | When | What to pass |
|--------|--------------|------|-------------|
| `show_banner()` | end | Startup | `_set_terminal_title()` — no args, uses fallback |
| `new_session()` | after session ID set | `/new`, `/reset` | `_set_terminal_title(title)` — new title or None |
| `_handle_resume_command()` | after session switch | `/resume` | `_set_terminal_title()` — picks up DB title |
| Branch handler | after branch created | `/branch` | `_set_terminal_title(branch_title)` |
| `/title` handler | after `set_session_title` | `/title` | `_set_terminal_title(new_title)` |
| `_init_agent()` pending title | after title applied | First message | `_set_terminal_title(self._pending_title)` |
| `chat()` | before `return response` | After each agent turn | `_set_terminal_title()` — picks up first message |

### Patching cli.py Safely

- **Indentation matters.** cli.py uses 4-space indentation. `HermesCLI` methods use
  4 spaces (one level). Method bodies use 8 spaces. Nested blocks use 12.
- **Always syntax-check after patching:**
  ```bash
  cd ~/.hermes/hermes-agent
  python3 -c "import py_compile; py_compile.compile('cli.py', doraise=True)"
  ```
- **Pre-existing warnings are OK.** The `SyntaxWarning: 'return' in a 'finally' block`
  at line ~10600 is pre-existing — ignore it.
- **Methods vs nested functions:** Adding `def _method(self, ...)` inside `__init__`
  creates a nested function where `self` shadows the instance. Always add methods
  at class level (between existing methods), not inside `__init__`.
- **Use `rtk grep -n`** to find line numbers before patching — they shift with each edit.

## Pitfalls

- **`SessionDB` may be None.** Agent initialization can fail, leaving `self._session_db`
  as None. Always guard with `if self._session_db:` before calling DB methods.
- **`conversation_history` is empty at startup.** The first-user-message fallback
  won't work until after the first `chat()` call. Startup title will show session ID.
- **Don't add methods inside `__init__`.** Python treats them as nested functions
  where `self` becomes a regular parameter, not the instance reference.
- **The `finally` block at line ~10600** in `chat()` has a `return` statement that
  triggers a SyntaxWarning. This is pre-existing — don't try to fix it as part of
  other changes.
- **Terminal title escape sequences are invisible.** They produce no visible output
  but are interpreted by the terminal. If titles aren't changing, verify the terminal
  emulator supports OSC sequences (almost all modern ones do).
- **Hermes updates WIPE custom patches to cli.py.** When `hermes update` runs, it
  replaces the entire install directory with a fresh git pull — any custom changes
  to `cli.py` are lost. Before updating, Hermes backs up modified files to
  `~/hermes-backups/pre-update-<timestamp>/`. The backup includes:
  - `cli.py.original` — pristine pre-update file
  - `cli.py.modified` — your version with custom patches
  - `cli-py.patch` — unified diff you can re-apply
  To restore after an update: `cd ~/.hermes/hermes-agent && git apply ~/hermes-backups/pre-update-*/cli-py.patch` — but
  expect line-number conflicts if upstream changed the file significantly.
- **Line numbers SHIFT between Hermes versions.** The file grows/shrinks with each
  update (e.g., 14,822 → 16,455 lines between v0.17 and v0.18.2). Always use
  `rtk grep -n "pattern" cli.py` to find current line numbers before patching;
  never rely on line numbers from a previous session.
- **CLI `_set_terminal_title()` and the TUI title are SEPARATE paths.**
  The CLI's `_set_terminal_title()` works for `hermes` (plain CLI) sessions.
  The Ink/React TUI (`hermes --tui`) sets the terminal title via its own
  `useTerminalTitle()` hook in `ui-tui/src/app/useMainApp.ts`, which pulls
  session titles from the `session.active_list` gateway RPC. If both are
  running in the same terminal, the TUI one wins. The desktop app uses yet
  another path — pane headers come from `paneFor(paneId)?.title` in zone
  definitions, not from session titles.
