initial commit

This commit is contained in:
ray
2026-07-12 10:17:17 -04:00
commit dab5a4ebc6
1424 changed files with 330463 additions and 0 deletions
@@ -0,0 +1,3 @@
---
description: Skills for spawning and orchestrating autonomous AI coding agents and multi-agent workflows — running independent agent processes, delegating tasks, and coordinating parallel workstreams.
---
@@ -0,0 +1,801 @@
---
name: claude-code
description: "Delegate coding to Claude Code CLI (features, PRs)."
version: 2.2.0
author: Hermes Agent + Teknium
license: MIT
platforms: [linux, macos, windows]
metadata:
hermes:
tags: [Coding-Agent, Claude, Anthropic, Code-Review, Refactoring, PTY, Automation]
related_skills: [codex, hermes-agent, opencode]
---
# Claude Code — Hermes Orchestration Guide
Delegate coding tasks to [Claude Code](https://code.claude.com/docs/en/cli-reference) (Anthropic's autonomous coding agent CLI) via the Hermes terminal. Claude Code v2.x can read files, write code, run shell commands, spawn subagents, and manage git workflows autonomously.
## Prerequisites
- **Install:** `npm install -g @anthropic-ai/claude-code`
- **Auth:** run `claude` once to log in (browser OAuth for Pro/Max, or set `ANTHROPIC_API_KEY`)
- **Console auth:** `claude auth login --console` for API key billing
- **SSO auth:** `claude auth login --sso` for Enterprise
- **Check status:** `claude auth status` (JSON) or `claude auth status --text` (human-readable)
- **Health check:** `claude doctor` — checks auto-updater and installation health
- **Version check:** `claude --version` (requires v2.x+)
- **Update:** `claude update` or `claude upgrade`
## Two Orchestration Modes
Hermes interacts with Claude Code in two fundamentally different ways. Choose based on the task.
### Mode 1: Print Mode (`-p`) — Non-Interactive (PREFERRED for most tasks)
Print mode runs a one-shot task, returns the result, and exits. No PTY needed. No interactive prompts. This is the cleanest integration path.
```
terminal(command="claude -p 'Add error handling to all API calls in src/' --allowedTools 'Read,Edit' --max-turns 10", workdir="/path/to/project", timeout=120)
```
**When to use print mode:**
- One-shot coding tasks (fix a bug, add a feature, refactor)
- CI/CD automation and scripting
- Structured data extraction with `--json-schema`
- Piped input processing (`cat file | claude -p "analyze this"`)
- Any task where you don't need multi-turn conversation
**Print mode skips ALL interactive dialogs** — no workspace trust prompt, no permission confirmations. This makes it ideal for automation.
### Mode 2: Interactive PTY via tmux — Multi-Turn Sessions
Interactive mode gives you a full conversational REPL where you can send follow-up prompts, use slash commands, and watch Claude work in real time. **Requires tmux orchestration.**
```
# Start a tmux session
terminal(command="tmux new-session -d -s claude-work -x 140 -y 40")
# Launch Claude Code inside it
terminal(command="tmux send-keys -t claude-work 'cd /path/to/project && claude' Enter")
# Wait for startup, then send your task
# (after ~3-5 seconds for the welcome screen)
terminal(command="sleep 5 && tmux send-keys -t claude-work 'Refactor the auth module to use JWT tokens' Enter")
# Monitor progress by capturing the pane
terminal(command="sleep 15 && tmux capture-pane -t claude-work -p -S -50")
# Send follow-up tasks
terminal(command="tmux send-keys -t claude-work 'Now add unit tests for the new JWT code' Enter")
# Exit when done
terminal(command="tmux send-keys -t claude-work '/exit' Enter")
```
**When to use interactive mode:**
- Multi-turn iterative work (refactor → review → fix → test cycle)
- Tasks requiring human-in-the-loop decisions
- Exploratory coding sessions
- When you need to use Claude's slash commands (`/compact`, `/review`, `/model`)
## PTY Dialog Handling (CRITICAL for Interactive Mode)
Claude Code presents up to two confirmation dialogs on first launch. You MUST handle these via tmux send-keys:
### Dialog 1: Workspace Trust (first visit to a directory)
```
1. Yes, I trust this folder ← DEFAULT (just press Enter)
2. No, exit
```
**Handling:** `tmux send-keys -t <session> Enter` — default selection is correct.
### Dialog 2: Bypass Permissions Warning (only with --dangerously-skip-permissions)
```
1. No, exit ← DEFAULT (WRONG choice!)
2. Yes, I accept
```
**Handling:** Must navigate DOWN first, then Enter:
```
tmux send-keys -t <session> Down && sleep 0.3 && tmux send-keys -t <session> Enter
```
### Robust Dialog Handling Pattern
```
# Launch with permissions bypass
terminal(command="tmux send-keys -t claude-work 'claude --dangerously-skip-permissions \"your task\"' Enter")
# Handle trust dialog (Enter for default "Yes")
terminal(command="sleep 4 && tmux send-keys -t claude-work Enter")
# Handle permissions dialog (Down then Enter for "Yes, I accept")
terminal(command="sleep 3 && tmux send-keys -t claude-work Down && sleep 0.3 && tmux send-keys -t claude-work Enter")
# Now wait for Claude to work
terminal(command="sleep 15 && tmux capture-pane -t claude-work -p -S -60")
```
**Note:** After the first trust acceptance for a directory, the trust dialog won't appear again. Only the permissions dialog recurs each time you use `--dangerously-skip-permissions`.
## CLI Subcommands
| Subcommand | Purpose |
|------------|---------|
| `claude` | Start interactive REPL |
| `claude "query"` | Start REPL with initial prompt |
| `claude -p "query"` | Print mode (non-interactive, exits when done) |
| `cat file \| claude -p "query"` | Pipe content as stdin context |
| `claude -c` | Continue the most recent conversation in this directory |
| `claude -r "id"` | Resume a specific session by ID or name |
| `claude auth login` | Sign in (add `--console` for API billing, `--sso` for Enterprise) |
| `claude auth status` | Check login status (returns JSON; `--text` for human-readable) |
| `claude mcp add <name> -- <cmd>` | Add an MCP server |
| `claude mcp list` | List configured MCP servers |
| `claude mcp remove <name>` | Remove an MCP server |
| `claude agents` | List configured agents |
| `claude doctor` | Run health checks on installation and auto-updater |
| `claude update` / `claude upgrade` | Update Claude Code to latest version |
| `claude remote-control` | Start server to control Claude from claude.ai or mobile app |
| `claude install [target]` | Install native build (stable, latest, or specific version) |
| `claude setup-token` | Set up long-lived auth token (requires subscription) |
| `claude plugin` / `claude plugins` | Manage Claude Code plugins |
| `claude auto-mode` | Inspect auto mode classifier configuration |
## Print Mode Deep Dive
### Structured JSON Output
```
terminal(command="claude -p 'Analyze auth.py for security issues' --output-format json --max-turns 5", workdir="/project", timeout=120)
```
Returns a JSON object with:
```json
{
"type": "result",
"subtype": "success",
"result": "The analysis text...",
"session_id": "75e2167f-...",
"num_turns": 3,
"total_cost_usd": 0.0787,
"duration_ms": 10276,
"stop_reason": "end_turn",
"terminal_reason": "completed",
"usage": { "input_tokens": 5, "output_tokens": 603, ... },
"modelUsage": { "claude-sonnet-4-6": { "costUSD": 0.078, "contextWindow": 200000 } }
}
```
**Key fields:** `session_id` for resumption, `num_turns` for agentic loop count, `total_cost_usd` for spend tracking, `subtype` for success/error detection (`success`, `error_max_turns`, `error_budget`).
### Streaming JSON Output
For real-time token streaming, use `stream-json` with `--verbose`:
```
terminal(command="claude -p 'Write a summary' --output-format stream-json --verbose --include-partial-messages", timeout=60)
```
Returns newline-delimited JSON events. Filter with jq for live text:
```
claude -p "Explain X" --output-format stream-json --verbose --include-partial-messages | \
jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'
```
Stream events include `system/api_retry` with `attempt`, `max_retries`, and `error` fields (e.g., `rate_limit`, `billing_error`).
### Bidirectional Streaming
For real-time input AND output streaming:
```
claude -p "task" --input-format stream-json --output-format stream-json --replay-user-messages
```
`--replay-user-messages` re-emits user messages on stdout for acknowledgment.
### Piped Input
```
# Pipe a file for analysis
terminal(command="cat src/auth.py | claude -p 'Review this code for bugs' --max-turns 1", timeout=60)
# Pipe multiple files
terminal(command="cat src/*.py | claude -p 'Find all TODO comments' --max-turns 1", timeout=60)
# Pipe command output
terminal(command="git diff HEAD~3 | claude -p 'Summarize these changes' --max-turns 1", timeout=60)
```
### JSON Schema for Structured Extraction
```
terminal(command="claude -p 'List all functions in src/' --output-format json --json-schema '{\"type\":\"object\",\"properties\":{\"functions\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"functions\"]}' --max-turns 5", workdir="/project", timeout=90)
```
Parse `structured_output` from the JSON result. Claude validates output against the schema before returning.
### Session Continuation
```
# Start a task
terminal(command="claude -p 'Start refactoring the database layer' --output-format json --max-turns 10 > /tmp/session.json", workdir="/project", timeout=180)
# Resume with session ID
terminal(command="claude -p 'Continue and add connection pooling' --resume $(cat /tmp/session.json | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"session_id\"])') --max-turns 5", workdir="/project", timeout=120)
# Or resume the most recent session in the same directory
terminal(command="claude -p 'What did you do last time?' --continue --max-turns 1", workdir="/project", timeout=30)
# Fork a session (new ID, keeps history)
terminal(command="claude -p 'Try a different approach' --resume <id> --fork-session --max-turns 10", workdir="/project", timeout=120)
```
### Bare Mode for CI/Scripting
```
terminal(command="claude --bare -p 'Run all tests and report failures' --allowedTools 'Read,Bash' --max-turns 10", workdir="/project", timeout=180)
```
`--bare` skips hooks, plugins, MCP discovery, and CLAUDE.md loading. Fastest startup. Requires `ANTHROPIC_API_KEY` (skips OAuth).
To selectively load context in bare mode:
| To load | Flag |
|---------|------|
| System prompt additions | `--append-system-prompt "text"` or `--append-system-prompt-file path` |
| Settings | `--settings <file-or-json>` |
| MCP servers | `--mcp-config <file-or-json>` |
| Custom agents | `--agents '<json>'` |
### Fallback Model for Overload
```
terminal(command="claude -p 'task' --fallback-model haiku --max-turns 5", timeout=90)
```
Automatically falls back to the specified model when the default is overloaded (print mode only).
## Complete CLI Flags Reference
### Session & Environment
| Flag | Effect |
|------|--------|
| `-p, --print` | Non-interactive one-shot mode (exits when done) |
| `-c, --continue` | Resume most recent conversation in current directory |
| `-r, --resume <id>` | Resume specific session by ID or name (interactive picker if no ID) |
| `--fork-session` | When resuming, create new session ID instead of reusing original |
| `--session-id <uuid>` | Use a specific UUID for the conversation |
| `--no-session-persistence` | Don't save session to disk (print mode only) |
| `--add-dir <paths...>` | Grant Claude access to additional working directories |
| `-w, --worktree [name]` | Run in an isolated git worktree at `.claude/worktrees/<name>` |
| `--tmux` | Create a tmux session for the worktree (requires `--worktree`) |
| `--ide` | Auto-connect to a valid IDE on startup |
| `--chrome` / `--no-chrome` | Enable/disable Chrome browser integration for web testing |
| `--from-pr [number]` | Resume session linked to a specific GitHub PR |
| `--file <specs...>` | File resources to download at startup (format: `file_id:relative_path`) |
### Model & Performance
| Flag | Effect |
|------|--------|
| `--model <alias>` | Model selection: `sonnet`, `opus`, `haiku`, or full name like `claude-sonnet-4-6` |
| `--effort <level>` | Reasoning depth: `low`, `medium`, `high`, `max`, `auto` | Both |
| `--max-turns <n>` | Limit agentic loops (print mode only; prevents runaway) |
| `--max-budget-usd <n>` | Cap API spend in dollars (print mode only) |
| `--fallback-model <model>` | Auto-fallback when default model is overloaded (print mode only) |
| `--betas <betas...>` | Beta headers to include in API requests (API key users only) |
### Permission & Safety
| Flag | Effect |
|------|--------|
| `--dangerously-skip-permissions` | Auto-approve ALL tool use (file writes, bash, network, etc.) |
| `--allow-dangerously-skip-permissions` | Enable bypass as an *option* without enabling it by default |
| `--permission-mode <mode>` | `default`, `acceptEdits`, `plan`, `auto`, `dontAsk`, `bypassPermissions` |
| `--allowedTools <tools...>` | Whitelist specific tools (comma or space-separated) |
| `--disallowedTools <tools...>` | Blacklist specific tools |
| `--tools <tools...>` | Override built-in tool set (`""` = none, `"default"` = all, or tool names) |
### Output & Input Format
| Flag | Effect |
|------|--------|
| `--output-format <fmt>` | `text` (default), `json` (single result object), `stream-json` (newline-delimited) |
| `--input-format <fmt>` | `text` (default) or `stream-json` (real-time streaming input) |
| `--json-schema <schema>` | Force structured JSON output matching a schema |
| `--verbose` | Full turn-by-turn output |
| `--include-partial-messages` | Include partial message chunks as they arrive (stream-json + print) |
| `--replay-user-messages` | Re-emit user messages on stdout (stream-json bidirectional) |
### System Prompt & Context
| Flag | Effect |
|------|--------|
| `--append-system-prompt <text>` | **Add** to the default system prompt (preserves built-in capabilities) |
| `--append-system-prompt-file <path>` | **Add** file contents to the default system prompt |
| `--system-prompt <text>` | **Replace** the entire system prompt (use --append instead usually) |
| `--system-prompt-file <path>` | **Replace** the system prompt with file contents |
| `--bare` | Skip hooks, plugins, MCP discovery, CLAUDE.md, OAuth (fastest startup) |
| `--agents '<json>'` | Define custom subagents dynamically as JSON |
| `--mcp-config <path>` | Load MCP servers from JSON file (repeatable) |
| `--strict-mcp-config` | Only use MCP servers from `--mcp-config`, ignoring all other MCP configs |
| `--settings <file-or-json>` | Load additional settings from a JSON file or inline JSON |
| `--setting-sources <sources>` | Comma-separated sources to load: `user`, `project`, `local` |
| `--plugin-dir <paths...>` | Load plugins from directories for this session only |
| `--disable-slash-commands` | Disable all skills/slash commands |
### Debugging
| Flag | Effect |
|------|--------|
| `-d, --debug [filter]` | Enable debug logging with optional category filter (e.g., `"api,hooks"`, `"!1p,!file"`) |
| `--debug-file <path>` | Write debug logs to file (implicitly enables debug mode) |
### Agent Teams
| Flag | Effect |
|------|--------|
| `--teammate-mode <mode>` | How agent teams display: `auto`, `in-process`, or `tmux` |
| `--brief` | Enable `SendUserMessage` tool for agent-to-user communication |
### Tool Name Syntax for --allowedTools / --disallowedTools
```
Read # All file reading
Edit # File editing (existing files)
Write # File creation (new files)
Bash # All shell commands
Bash(git *) # Only git commands
Bash(git commit *) # Only git commit commands
Bash(npm run lint:*) # Pattern matching with wildcards
WebSearch # Web search capability
WebFetch # Web page fetching
mcp__<server>__<tool> # Specific MCP tool
```
## Settings & Configuration
### Settings Hierarchy (highest to lowest priority)
1. **CLI flags** — override everything
2. **Local project:** `.claude/settings.local.json` (personal, gitignored)
3. **Project:** `.claude/settings.json` (shared, git-tracked)
4. **User:** `~/.claude/settings.json` (global)
### Permissions in Settings
```json
{
"permissions": {
"allow": ["Bash(npm run lint:*)", "WebSearch", "Read"],
"ask": ["Write(*.ts)", "Bash(git push*)"],
"deny": ["Read(.env)", "Bash(rm -rf *)"]
}
}
```
### Memory Files (CLAUDE.md) Hierarchy
1. **Global:** `~/.claude/CLAUDE.md` — applies to all projects
2. **Project:** `./CLAUDE.md` — project-specific context (git-tracked)
3. **Local:** `.claude/CLAUDE.local.md` — personal project overrides (gitignored)
Use the `#` prefix in interactive mode to quickly add to memory: `# Always use 2-space indentation`.
## Interactive Session: Slash Commands
### Session & Context
| Command | Purpose |
|---------|---------|
| `/help` | Show all commands (including custom and MCP commands) |
| `/compact [focus]` | Compress context to save tokens; CLAUDE.md survives compaction. E.g., `/compact focus on auth logic` |
| `/clear` | Wipe conversation history for a fresh start |
| `/context` | Visualize context usage as a colored grid with optimization tips |
| `/cost` | View token usage with per-model and cache-hit breakdowns |
| `/resume` | Switch to or resume a different session |
| `/rewind` | Revert to a previous checkpoint in conversation or code |
| `/btw <question>` | Ask a side question without adding to context cost |
| `/status` | Show version, connectivity, and session info |
| `/todos` | List tracked action items from the conversation |
| `/exit` or `Ctrl+D` | End session |
### Development & Review
| Command | Purpose |
|---------|---------|
| `/review` | Request code review of current changes |
| `/security-review` | Perform security analysis of current changes |
| `/plan [description]` | Enter Plan mode with auto-start for task planning |
| `/loop [interval]` | Schedule recurring tasks within the session |
| `/batch` | Auto-create worktrees for large parallel changes (5-30 worktrees) |
### Configuration & Tools
| Command | Purpose |
|---------|---------|
| `/model [model]` | Switch models mid-session (use arrow keys to adjust effort) |
| `/effort [level]` | Set reasoning effort: `low`, `medium`, `high`, `max`, or `auto` |
| `/init` | Create a CLAUDE.md file for project memory |
| `/memory` | Open CLAUDE.md for editing |
| `/config` | Open interactive settings configuration |
| `/permissions` | View/update tool permissions |
| `/agents` | Manage specialized subagents |
| `/mcp` | Interactive UI to manage MCP servers |
| `/add-dir` | Add additional working directories (useful for monorepos) |
| `/usage` | Show plan limits and rate limit status |
| `/voice` | Enable push-to-talk voice mode (20 languages; hold Space to record, release to send) |
| `/release-notes` | Interactive picker for version release notes |
### Custom Slash Commands
Create `.claude/commands/<name>.md` (project-shared) or `~/.claude/commands/<name>.md` (personal):
```markdown
# .claude/commands/deploy.md
Run the deploy pipeline:
1. Run all tests
2. Build the Docker image
3. Push to registry
4. Update the $ARGUMENTS environment (default: staging)
```
Usage: `/deploy production``$ARGUMENTS` is replaced with the user's input.
### Skills (Natural Language Invocation)
Unlike slash commands (manually invoked), skills in `.claude/skills/` are markdown guides that Claude invokes automatically via natural language when the task matches:
```markdown
# .claude/skills/database-migration.md
When asked to create or modify database migrations:
1. Use Alembic for migration generation
2. Always create a rollback function
3. Test migrations against a local database copy
```
## Interactive Session: Keyboard Shortcuts
### General Controls
| Key | Action |
|-----|--------|
| `Ctrl+C` | Cancel current input or generation |
| `Ctrl+D` | Exit session |
| `Ctrl+R` | Reverse search command history |
| `Ctrl+B` | Background a running task |
| `Ctrl+V` | Paste image into conversation |
| `Ctrl+O` | Transcript mode — see Claude's thinking process |
| `Ctrl+G` or `Ctrl+X Ctrl+E` | Open prompt in external editor |
| `Esc Esc` | Rewind conversation or code state / summarize |
### Mode Toggles
| Key | Action |
|-----|--------|
| `Shift+Tab` | Cycle permission modes (Normal → Auto-Accept → Plan) |
| `Alt+P` | Switch model |
| `Alt+T` | Toggle thinking mode |
| `Alt+O` | Toggle Fast Mode |
### Multiline Input
| Key | Action |
|-----|--------|
| `\` + `Enter` | Quick newline |
| `Shift+Enter` | Newline (alternative) |
| `Ctrl+J` | Newline (alternative) |
### Input Prefixes
| Prefix | Action |
|--------|--------|
| `!` | Execute bash directly, bypassing AI (e.g., `!npm test`). Use `!` alone to toggle shell mode. |
| `@` | Reference files/directories with autocomplete (e.g., `@./src/api/`) |
| `#` | Quick add to CLAUDE.md memory (e.g., `# Use 2-space indentation`) |
| `/` | Slash commands |
### Pro Tip: "ultrathink"
Use the keyword "ultrathink" in your prompt for maximum reasoning effort on a specific turn. This triggers the deepest thinking mode regardless of the current `/effort` setting.
## PR Review Pattern
### Quick Review (Print Mode)
```
terminal(command="cd /path/to/repo && git diff main...feature-branch | claude -p 'Review this diff for bugs, security issues, and style problems. Be thorough.' --max-turns 1", timeout=60)
```
### Deep Review (Interactive + Worktree)
```
terminal(command="tmux new-session -d -s review -x 140 -y 40")
terminal(command="tmux send-keys -t review 'cd /path/to/repo && claude -w pr-review' Enter")
terminal(command="sleep 5 && tmux send-keys -t review Enter") # Trust dialog
terminal(command="sleep 2 && tmux send-keys -t review 'Review all changes vs main. Check for bugs, security issues, race conditions, and missing tests.' Enter")
terminal(command="sleep 30 && tmux capture-pane -t review -p -S -60")
```
### PR Review from Number
```
terminal(command="claude -p 'Review this PR thoroughly' --from-pr 42 --max-turns 10", workdir="/path/to/repo", timeout=120)
```
### Claude Worktree with tmux
```
terminal(command="claude -w feature-x --tmux", workdir="/path/to/repo")
```
Creates an isolated git worktree at `.claude/worktrees/feature-x` AND a tmux session for it. Uses iTerm2 native panes when available; add `--tmux=classic` for traditional tmux.
## Parallel Claude Instances
Run multiple independent Claude tasks simultaneously:
```
# Task 1: Fix backend
terminal(command="tmux new-session -d -s task1 -x 140 -y 40 && tmux send-keys -t task1 'cd ~/project && claude -p \"Fix the auth bug in src/auth.py\" --allowedTools \"Read,Edit\" --max-turns 10' Enter")
# Task 2: Write tests
terminal(command="tmux new-session -d -s task2 -x 140 -y 40 && tmux send-keys -t task2 'cd ~/project && claude -p \"Write integration tests for the API endpoints\" --allowedTools \"Read,Write,Bash\" --max-turns 15' Enter")
# Task 3: Update docs
terminal(command="tmux new-session -d -s task3 -x 140 -y 40 && tmux send-keys -t task3 'cd ~/project && claude -p \"Update README.md with the new API endpoints\" --allowedTools \"Read,Edit\" --max-turns 5' Enter")
# Monitor all
terminal(command="sleep 30 && for s in task1 task2 task3; do echo '=== '$s' ==='; tmux capture-pane -t $s -p -S -5 2>/dev/null; done")
```
## CLAUDE.md — Project Context File
Claude Code auto-loads `CLAUDE.md` from the project root. Use it to persist project context:
```markdown
# Project: My API
## Architecture
- FastAPI backend with SQLAlchemy ORM
- PostgreSQL database, Redis cache
- pytest for testing with 90% coverage target
## Key Commands
- `make test` — run full test suite
- `make lint` — ruff + mypy
- `make dev` — start dev server on :8000
## Code Standards
- Type hints on all public functions
- Docstrings in Google style
- 2-space indentation for YAML, 4-space for Python
- No wildcard imports
```
**Be specific.** Instead of "Write good code", use "Use 2-space indentation for JS" or "Name test files with `.test.ts` suffix." Specific instructions save correction cycles.
### Rules Directory (Modular CLAUDE.md)
For projects with many rules, use the rules directory instead of one massive CLAUDE.md:
- **Project rules:** `.claude/rules/*.md` — team-shared, git-tracked
- **User rules:** `~/.claude/rules/*.md` — personal, global
Each `.md` file in the rules directory is loaded as additional context. This is cleaner than cramming everything into a single CLAUDE.md.
### Auto-Memory
Claude automatically stores learned project context in `~/.claude/projects/<project>/memory/`.
- **Limit:** 25KB or 200 lines per project
- This is separate from CLAUDE.md — it's Claude's own notes about the project, accumulated across sessions
## Custom Subagents
Define specialized agents in `.claude/agents/` (project), `~/.claude/agents/` (personal), or via `--agents` CLI flag (session):
### Agent Location Priority
1. `.claude/agents/` — project-level, team-shared
2. `--agents` CLI flag — session-specific, dynamic
3. `~/.claude/agents/` — user-level, personal
### Creating an Agent
```markdown
# .claude/agents/security-reviewer.md
---
name: security-reviewer
description: Security-focused code review
model: opus
tools: [Read, Bash]
---
You are a senior security engineer. Review code for:
- Injection vulnerabilities (SQL, XSS, command injection)
- Authentication/authorization flaws
- Secrets in code
- Unsafe deserialization
```
Invoke via: `@security-reviewer review the auth module`
### Dynamic Agents via CLI
```
terminal(command="claude --agents '{\"reviewer\": {\"description\": \"Reviews code\", \"prompt\": \"You are a code reviewer focused on performance\"}}' -p 'Use @reviewer to check auth.py'", timeout=120)
```
Claude can orchestrate multiple agents: "Use @db-expert to optimize queries, then @security to audit the changes."
## Hooks — Automation on Events
Configure in `.claude/settings.json` (project) or `~/.claude/settings.json` (global):
```json
{
"hooks": {
"PostToolUse": [{
"matcher": "Write(*.py)",
"hooks": [{"type": "command", "command": "ruff check --fix $CLAUDE_FILE_PATHS"}]
}],
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -q 'rm -rf'; then echo 'Blocked!' && exit 2; fi"}]
}],
"Stop": [{
"hooks": [{"type": "command", "command": "echo 'Claude finished a response' >> /tmp/claude-activity.log"}]
}]
}
}
```
### All 8 Hook Types
| Hook | When it fires | Common use |
|------|--------------|------------|
| `UserPromptSubmit` | Before Claude processes a user prompt | Input validation, logging |
| `PreToolUse` | Before tool execution | Security gates, block dangerous commands (exit 2 = block) |
| `PostToolUse` | After a tool finishes | Auto-format code, run linters |
| `Notification` | On permission requests or input waits | Desktop notifications, alerts |
| `Stop` | When Claude finishes a response | Completion logging, status updates |
| `SubagentStop` | When a subagent completes | Agent orchestration |
| `PreCompact` | Before context memory is cleared | Backup session transcripts |
| `SessionStart` | When a session begins | Load dev context (e.g., `git status`) |
### Hook Environment Variables
| Variable | Content |
|----------|---------|
| `CLAUDE_PROJECT_DIR` | Current project path |
| `CLAUDE_FILE_PATHS` | Files being modified |
| `CLAUDE_TOOL_INPUT` | Tool parameters as JSON |
### Security Hook Examples
```json
{
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'rm -rf|git push.*--force|:(){ :|:& };:'; then echo 'Dangerous command blocked!' && exit 2; fi"}]
}]
}
```
## MCP Integration
Add external tool servers for databases, APIs, and services:
```
# GitHub integration
terminal(command="claude mcp add -s user github -- npx @modelcontextprotocol/server-github", timeout=30)
# PostgreSQL queries
terminal(command="claude mcp add -s local postgres -- npx @anthropic-ai/server-postgres --connection-string postgresql://localhost/mydb", timeout=30)
# Puppeteer for web testing
terminal(command="claude mcp add puppeteer -- npx @anthropic-ai/server-puppeteer", timeout=30)
```
### MCP Scopes
| Flag | Scope | Storage |
|------|-------|---------|
| `-s user` | Global (all projects) | `~/.claude.json` |
| `-s local` | This project (personal) | `.claude/settings.local.json` (gitignored) |
| `-s project` | This project (team-shared) | `.claude/settings.json` (git-tracked) |
### MCP in Print/CI Mode
```
terminal(command="claude --bare -p 'Query database' --mcp-config mcp-servers.json --strict-mcp-config", timeout=60)
```
`--strict-mcp-config` ignores all MCP servers except those from `--mcp-config`.
Reference MCP resources in chat: `@github:issue://123`
### MCP Limits & Tuning
- **Tool descriptions:** 2KB cap per server for tool descriptions and server instructions
- **Result size:** Default capped; use `maxResultSizeChars` annotation to allow up to **500K** characters for large outputs
- **Output tokens:** `export MAX_MCP_OUTPUT_TOKENS=50000` — cap output from MCP servers to prevent context flooding
- **Transports:** `stdio` (local process), `http` (remote), `sse` (server-sent events)
## Monitoring Interactive Sessions
### Reading the TUI Status
```
# Periodic capture to check if Claude is still working or waiting for input
terminal(command="tmux capture-pane -t dev -p -S -10")
```
Look for these indicators:
- `` at bottom = waiting for your input (Claude is done or asking a question)
- `●` lines = Claude is actively using tools (reading, writing, running commands)
- `⏵⏵ bypass permissions on` = status bar showing permissions mode
- `◐ medium · /effort` = current effort level in status bar
- `ctrl+o to expand` = tool output was truncated (can be expanded interactively)
### Context Window Health
Use `/context` in interactive mode to see a colored grid of context usage. Key thresholds:
- **< 70%** — Normal operation, full precision
- **70-85%** — Precision starts dropping, consider `/compact`
- **> 85%** — Hallucination risk spikes significantly, use `/compact` or `/clear`
## Environment Variables
| Variable | Effect |
|----------|--------|
| `ANTHROPIC_API_KEY` | API key for authentication (alternative to OAuth) |
| `CLAUDE_CODE_EFFORT_LEVEL` | Default effort: `low`, `medium`, `high`, `max`, or `auto` |
| `MAX_THINKING_TOKENS` | Cap thinking tokens (set to `0` to disable thinking entirely) |
| `MAX_MCP_OUTPUT_TOKENS` | Cap output from MCP servers (default varies; set e.g., `50000`) |
| `CLAUDE_CODE_NO_FLICKER=1` | Enable alt-screen rendering to eliminate terminal flicker |
| `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` | Strip credentials from sub-processes for security |
## Cost & Performance Tips
1. **Use `--max-turns`** in print mode to prevent runaway loops. Start with 5-10 for most tasks.
2. **Use `--max-budget-usd`** for cost caps. Note: minimum ~$0.05 for system prompt cache creation.
3. **Use `--effort low`** for simple tasks (faster, cheaper). `high` or `max` for complex reasoning.
4. **Use `--bare`** for CI/scripting to skip plugin/hook discovery overhead.
5. **Use `--allowedTools`** to restrict to only what's needed (e.g., `Read` only for reviews).
6. **Use `/compact`** in interactive sessions when context gets large.
7. **Pipe input** instead of having Claude read files when you just need analysis of known content.
8. **Use `--model haiku`** for simple tasks (cheaper) and `--model opus` for complex multi-step work.
9. **Use `--fallback-model haiku`** in print mode to gracefully handle model overload.
10. **Start new sessions for distinct tasks** — sessions last 5 hours; fresh context is more efficient.
11. **Use `--no-session-persistence`** in CI to avoid accumulating saved sessions on disk.
## Pitfalls & Gotchas
1. **Interactive mode REQUIRES tmux** — Claude Code is a full TUI app. Using `pty=true` alone in Hermes terminal works but tmux gives you `capture-pane` for monitoring and `send-keys` for input, which is essential for orchestration.
2. **`--dangerously-skip-permissions` dialog defaults to "No, exit"** — you must send Down then Enter to accept. Print mode (`-p`) skips this entirely.
3. **`--max-budget-usd` minimum is ~$0.05** — system prompt cache creation alone costs this much. Setting lower will error immediately.
4. **`--max-turns` is print-mode only** — ignored in interactive sessions.
5. **Claude may use `python` instead of `python3`** — on systems without a `python` symlink, Claude's bash commands will fail on first try but it self-corrects.
6. **Session resumption requires same directory**`--continue` finds the most recent session for the current working directory.
7. **`--json-schema` needs enough `--max-turns`** — Claude must read files before producing structured output, which takes multiple turns.
8. **Trust dialog only appears once per directory** — first-time only, then cached.
9. **Background tmux sessions persist** — always clean up with `tmux kill-session -t <name>` when done.
10. **Slash commands (like `/commit`) only work in interactive mode** — in `-p` mode, describe the task in natural language instead.
11. **`--bare` skips OAuth** — requires `ANTHROPIC_API_KEY` env var or an `apiKeyHelper` in settings.
12. **Context degradation is real** — AI output quality measurably degrades above 70% context window usage. Monitor with `/context` and proactively `/compact`.
## Other Autonomous Coding Agents
The orchestration patterns documented above (PTY via tmux, background monitoring with `process()` tool, worktree isolation, PR review flow) apply to other coding agents too. Quick-reference tables below for **Codex** (OpenAI) and **OpenCode** (provider-agnostic open-source).
### Codex CLI (OpenAI)
```bash
# Install
npm install -g @openai/codex
# Auth: set OPENAI_API_KEY or use OAuth (hermes auth add openai-codex)
# Must run inside a git repository
```
| Task | Command |
|------|---------|
| One-shot task | `codex exec "description"` |
| Auto-approve changes | `codex exec --full-auto "..."` |
| No sandbox (gateway context) | `codex exec --sandbox danger-full-access "..."` |
| PR review | `codex review --base origin/main` |
See `references/codex.md` for the detailed integration guide.
### OpenCode CLI
```bash
# Install
npm i -g opencode-ai@latest
brew install anomalyco/tap/opencode
# Auth: opencode auth login (or set OPENROUTER_API_KEY, etc.)
```
| Task | Command |
|------|---------|
| One-shot task | `opencode run 'description'` |
| With context files | `opencode run '...' -f config.yaml` |
| Force model | `opencode run '...' --model openrouter/anthropic/claude-sonnet-4` |
| Interactive session | `opencode` (requires pty=true) |
| Resume last session | `opencode -c` |
| PR review | `opencode pr <number>` |
| Session costs | `opencode stats` |
**Important:** Do NOT use `/exit` in OpenCode — it opens the agent selector. Use Ctrl+C (`\x03`) instead.
See `references/opencode.md` for the detailed integration guide.
### Orchestration Comparison
| Feature | Claude Code | Codex | OpenCode |
|---------|-------------|-------|----------|
| One-shot mode | `-p "prompt"` | `exec "prompt"` | `run 'prompt'` |
| PTY needed for interactive | Yes | Yes | Yes |
| Background + monitor | tmux + capture-pane | background=true + process | background=true + process |
| Worktree isolation | `-w` flag | `git worktree add` | separate workdir |
| Sandbox | By default (bubblewrap) | `--full-auto` / `--sandbox` | N/A |
| Model override | `--model <name>` | N/A | `--model <provider/model>` |
1. **Prefer print mode (`-p`) for single tasks** — cleaner, no dialog handling, structured output
2. **Use tmux for multi-turn interactive work** — the only reliable way to orchestrate the TUI
3. **Always set `workdir`** — keep Claude focused on the right project directory
4. **Set `--max-turns` in print mode** — prevents infinite loops and runaway costs
5. **Monitor tmux sessions** — use `tmux capture-pane -t <session> -p -S -50` to check progress
6. **Look for the `` prompt** — indicates Claude is waiting for input (done or asking a question)
7. **Clean up tmux sessions** — kill them when done to avoid resource leaks
8. **Report results to user** — after completion, summarize what Claude did and what changed
9. **Don't kill slow sessions** — Claude may be doing multi-step work; check progress instead
10. **Use `--allowedTools`** — restrict capabilities to what the task actually needs
@@ -0,0 +1,54 @@
# Codex CLI — Detailed Integration Guide
[Codex](https://github.com/openai/codex) is OpenAI's autonomous coding agent CLI. This reference covers auth, edge cases, and gateway-specific pitfalls.
## Auth
Codex can use `OPENAI_API_KEY` or OAuth. For Hermes itself, `model.provider: openai-codex` uses Hermes-managed Codex OAuth from `~/.hermes/auth.json` after `hermes auth add openai-codex`. For standalone CLI, a valid OAuth session may live under `~/.codex/auth.json` — do not treat a missing `OPENAI_API_KEY` alone as proof that Codex auth is missing.
## One-Shot Tasks
```bash
terminal(command="codex exec 'Add dark mode toggle to settings'", workdir="~/project", pty=true)
```
For scratch work (Codex needs a git repo):
```bash
cd $(mktemp -d) && git init && codex exec 'Build a snake game in Python'
```
## Background Mode (Long Tasks)
```bash
terminal(command="codex exec --full-auto 'Refactor the auth module'", workdir="~/project", background=true, pty=true)
# Returns session_id — monitor with process(action="poll"|"log")
```
## Hermes Gateway Caveat
When invoking Codex CLI from a gateway/service context (Telegram-driven sessions), Codex `workspace-write` sandboxing may fail due to bubblewrap/user-namespace restrictions. Prefer:
```bash
codex exec --sandbox danger-full-access "<task>"
```
Use process boundaries as the safety layer: explicit `workdir`, clean git status before launch, narrow task prompts, `git diff` review, targeted tests.
## Parallel Issue Fixing with Worktrees
```bash
# Create worktrees
git worktree add -b fix/issue-78 /tmp/issue-78 main
# Launch Codex in each
terminal(command="codex --yolo exec 'Fix issue #78'", workdir="/tmp/issue-78", background=true, pty=true)
```
## Key Flags
| Flag | Effect |
|------|--------|
| `exec "prompt"` | One-shot execution, exits when done |
| `--full-auto` | Sandboxed but auto-approves file changes |
| `--yolo` | No sandbox, no approvals (fastest, most dangerous) |
| `--sandbox danger-full-access` | No sandbox; useful when bubblewrap fails |
@@ -0,0 +1,73 @@
# OpenCode CLI — Detailed Integration Guide
[OpenCode](https://opencode.ai) is a provider-agnostic, open-source AI coding agent. This reference covers binary resolution, session management, and key pitfalls.
## Binary Resolution
Shell environments may resolve different OpenCode binaries. Check:
```bash
terminal(command="which -a opencode")
terminal(command="opencode --version")
```
If needed, pin an explicit binary path: `$HOME/.opencode/bin/opencode run '...'`
## Interactive Sessions (Background)
```bash
# Start TUI in background
terminal(command="opencode", workdir="~/project", background=true, pty=true)
# Send a prompt
process(action="submit", session_id="<id>", data="Implement OAuth refresh flow")
# Monitor
process(action="log", session_id="<id>")
# Exit — use Ctrl+C, NOT /exit
process(action="write", session_id="<id>", data="\x03")
```
**Important:** Do NOT use `/exit` — it opens an agent selector instead. Use Ctrl+C (`\x03`) or `process(action="kill")`.
## TUI Keybindings
| Key | Action |
|-----|--------|
| Enter | Submit message |
| Tab | Switch between agents |
| Ctrl+P | Command palette |
| Ctrl+X L | Switch session |
| Ctrl+X M | Switch model |
| Ctrl+X N | New session |
| Ctrl+C | Exit |
## PR Review
```bash
terminal(command="opencode pr 42", workdir="~/project", pty=true)
```
Or review in a temporary clone for isolation.
## Session & Cost Management
```bash
opencode session list
opencode stats
opencode stats --days 7 --models anthropic/claude-sonnet-4
```
## Common Flags
| Flag | Use |
|------|-----|
| `run 'prompt'` | One-shot execution and exit |
| `-c` / `--continue` | Continue last session |
| `-s <id>` | Continue a specific session |
| `--agent <name>` | Choose agent (build or plan) |
| `--model provider/model` | Force specific model |
| `--thinking` | Show model thinking blocks |
| `--format json` | Machine-readable output |
| `-f <path>` | Attach file(s) to the message |
@@ -0,0 +1,105 @@
---
name: hermes-local-providers
description: "Configure Hermes Agent to use self-hosted, local LLM providers (Ollama, vLLM, llama.cpp, etc.) via OpenAI-compatible custom endpoints."
version: 1.0.0
author: agent
created_by: agent
metadata:
hermes:
tags: [hermes, ollama, local-models, configuration, self-hosted, vllm, llm-serving]
related_skills: [hermes-agent, llama-cpp, serving-llms-vllm, docker-gpu-acceleration]
---
# Hermes Local Providers
Configure Hermes Agent to use self-hosted / local LLMs that expose an OpenAI-compatible API endpoint.
## Supported Backends
| Backend | Typical Base URL | Auth | Notes |
|---------|-----------------|------|-------|
| Ollama | `http://localhost:11434/v1` | Placeholder | Most common for local use |
| vLLM | `http://localhost:8000/v1` | Optional API key | Production-scale serving |
| llama.cpp | `http://localhost:8080/v1` | None | Lightweight GGUF inference |
## Configuration
### 1. Set the custom provider
```bash
hermes config set model.provider custom
hermes config set model.base_url http://localhost:11434/v1
hermes config set model.api_key ollama
hermes config set model.default qwen2.5:14b
```
The provider name is **`custom`** — no suffix, no prefix. Hermes recognizes `custom` as a reserved provider that reads `model.base_url` and `model.api_key` directly from the config. Values like `custom:ollama` or `custom:vllm` will be rejected as "Unknown provider."
Ollama doesn't need a real API key, but `model.api_key` must be set to something non-empty. `ollama` (or any dummy string) works.
### 2. ⚠️ Verify the config persisted
`hermes config set` on `model.*` keys may report success (`✓ Set model.key = value`) but NOT actually write the value to `~/.hermes/config.yaml`. This is a known pitfall — always verify:
```bash
grep -A6 '^model:' ~/.hermes/config.yaml
```
Expected result:
```yaml
model:
default: qwen2.5:14b
provider: custom
base_url: http://localhost:11434/v1
api_key: ollama
```
> ⚠️ The provider must be **`custom`** (bare) — NOT `custom:ollama`, `custom:vllm`, or any suffix. Hermes only recognizes the bare `custom` as a reserved provider for user-defined endpoints. `custom:ollama` will be rejected as "Unknown provider."
If the old values are still showing, re-run the `hermes config set` commands and re-check. Retrying usually works; the silent failure appears to be intermittent.
### 3. Test the API independently
Before restarting the gateway, confirm the local backend is reachable:
```bash
curl -s http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen2.5:14b","messages":[{"role":"user","content":"Say hello in 3 words"}],"stream":false}'
```
If this fails, Ollama/vLLM/llama.cpp isn't running or the model isn't loaded.
### 4. Restart the gateway
```bash
hermes gateway restart
```
Config changes only take effect after a gateway restart. For CLI sessions, exit and relaunch.
## Pitfalls
- **Config set silent failure**: `hermes config set` for `model.*` keys (`provider`, `base_url`, `api_key`, `default`) may report "✓ Set" but not persist. Always grep the config file to confirm before assuming the change took effect.
- **Auth placeholder required**: A local Ollama endpoint doesn't authenticate, but Hermes requires `model.api_key` to be non-empty. Any dummy value works.
- **Gateway restart required**: Unlike some config changes, switching the model provider requires a full gateway restart. `/reset` or `/model` in-session won't pick up the new provider settings.
- **Auxiliary models stay unchanged**: Vision, compression, web extraction, title generation, and all other `auxiliary.*` subsystems keep their own provider config. Switching the main model to Ollama doesn't affect these — they'll continue using whatever provider/API key they were configured with (DeepSeek, OpenRouter, etc.).
- **Delegation has its own config**: The `delegation.*` section (`delegation.model`, `delegation.provider`, `delegation.base_url`) controls subagent models. To route subagents through the local provider too, set those separately:
```bash
hermes config set delegation.provider custom
hermes config set delegation.base_url http://localhost:11434/v1
hermes config set delegation.api_key ollama
hermes config set delegation.model qwen2.5:14b
```
- **Model must be downloaded**: `ollama list` shows installed models. If you set `model.default` to a model that isn't pulled, Ollama will pull it on first request (slow first call) or error if auto-pull is disabled.
## Verification
After restart, send a message to the agent. If you get "provider authentication error":
1. Check `~/.hermes/config.yaml` — the old provider values may have persisted
2. Re-run the `hermes config set` commands
3. Verify with `grep -A5 '^model:' ~/.hermes/config.yaml`
4. Restart gateway again
The "authentication error" is misleading — it usually means the API key/base URL weren't received by Hermes, which happens when `hermes config set` failed to persist.
@@ -0,0 +1,44 @@
# Ollama "Provider Authentication Error" Fix
## Symptom
After configuring a local provider (Ollama, vLLM, etc.), the agent reports "Provider authentication failed" on the next request. Gateway logs show: `Unknown provider 'custom:<name>'`.
## Root Cause
**Wrong provider name.** The Hermes custom endpoint provider is registered as **`custom`** (bare), not `custom:ollama`, `custom:vllm`, or any `custom:<name>` variant. The suffix format is not recognized — `custom:ollama` and `openai` are both rejected as unknown providers.
Secondary cause: `hermes config set` for `model.*` keys can silently fail to persist (reports "✓ Set" but file shows old values). Always verify with `grep -A5 '^model:' ~/.hermes/config.yaml`.
## Fix
1. Set the provider to `custom` (no suffix):
```bash
hermes config set model.provider custom
hermes config set model.base_url http://localhost:11434/v1
hermes config set model.api_key ollama
hermes config set model.default qwen2.5:14b
```
2. **Verify the file**:
```bash
grep -A6 '^model:' ~/.hermes/config.yaml
```
Must show `provider: custom`.
3. Test the Ollama API directly:
```bash
curl -s http://localhost:11434/v1/chat/completions -H "Content-Type: application/json" \
-d '{"model":"qwen2.5:14b","messages":[{"role":"user","content":"hi"}],"stream":false}'
```
4. Restart gateway (from OUTSIDE the gateway process — cannot restart from within):
```bash
hermes gateway restart
```
## Notes
- The provider `custom` is a reserved Hermes provider that reads `model.base_url` and `model.api_key` directly. It uses `transport="openai_chat"` (OpenAI-compatible API).
- If you set `custom` and still get the error, check with `grep` — the `config set` may have silently failed. Re-run the commands.
- Gateway restart must be done from a separate shell (SSH, desktop terminal) — not from within a gateway session.
@@ -0,0 +1,319 @@
---
name: hermes-telegram
description: Configure, troubleshoot, and manage Telegram integration for Hermes Agent — bot setup, .env configuration, gateway restart, and common failure modes.
version: 1.0.0
author: Hermes Agent
license: MIT
platforms: [linux, macos]
metadata:
hermes:
tags: [hermes, telegram, messaging, gateway, bot]
related_skills: [hermes-agent]
related_skills: [hermes-agent]
---
# Hermes Telegram Integration
Configure the Hermes Agent gateway to send and receive messages on Telegram. Covers bot creation, .env setup, allowed users, home channel, gateway restart, and troubleshooting.
## Prerequisites
- Hermes Agent gateway installed and running (`hermes gateway status` or `ps aux | grep 'hermes gateway run'`)
- A Telegram account to create and manage the bot
## Setup Steps
### 1. Create a Bot on Telegram
Message [@BotFather](https://t.me/BotFather) on Telegram:
```
/newbot
```
Follow the prompts to choose a name and username. BotFather will return a bot token like:
```
8971430276:AAFu...Amq4
```
Save this token — you'll need it in the next step.
### 2. Find Your Numeric IDs
You need numeric Telegram IDs, not usernames. Message [@userinfobot](https://t.me/userinfobot) on Telegram — it will reply with:
- **Your user ID** (a number like `123456789`) — used for `TELEGRAM_ALLOWED_USERS`
- For groups/channels: add the bot to the group, send a message, then use @userinfobot in the group to get the **chat ID**
Note: `TELEGRAM_ALLOWED_USERS` and `TELEGRAM_HOME_CHANNEL` require **numeric IDs**, not `@username` strings.
### 3. Configure .env
Edit the `.env` file. The default template has all Telegram variables **commented out** — you must uncomment and fill them in:
```bash
hermes config env-path # prints the path (typically ~/.hermes/.env)
```
The relevant lines (find them under the `# TELEGRAM INTEGRATION` section):
```env
TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_ALLOWED_USERS=123456789 # Comma-separated numeric user IDs
TELEGRAM_HOME_CHANNEL=123456789 # Numeric chat ID for cron deliveries
TELEGRAM_HOME_CHANNEL_NAME=@yourusername # Display name (optional, username is fine here)
```
**Important:** The default template uses a combined single-line format:
```
# TELEGRAM_BOT_TOKEN=*** TELEGRAM_ALLOWED_USERS=
```
When uncommenting, split this into **separate lines** as shown above — one variable per line. Use your editor or `sed` to do it cleanly.
For the `HOME_CHANNEL`, if you only want DM delivery (bot messages you directly), set it to your own user ID — same as `ALLOWED_USERS`.
### 4. Restart the Gateway
Configuration changes in `.env` are read at startup. Restart the gateway to pick them up:
```bash
hermes gateway restart
```
Or if running as a systemd user service:
```bash
systemctl --user restart hermes-gateway
```
#### Manual Restart (no systemd service)
If `systemctl --user list-units --type=service --all | grep hermes` returns nothing, the gateway was started manually. Kill and restart it:
```bash
# Find the gateway PID
ps aux | grep 'hermes gateway run'
# Kill it (use -9 if plain kill won't work)
kill -9 <PID>
# Wait for it to die
sleep 2
# Start fresh — MUST use background=true (nohup/disown is blocked by tool policy)
# In the Hermes CLI session:
# terminal(background=true, notify_on_complete=true, command="hermes gateway run")
```
**Important:** Do not use `nohup`, `disown`, or trailing `&` in a foreground terminal() call — the tool policy blocks shell-level background wrappers. Always use `terminal(background=true)` so Hermes tracks the process.
### 5. Verify It's Working
Send a message to your bot on Telegram. It should respond. Check the gateway logs for confirmation:
```bash
grep -i telegram ~/.hermes/logs/gateway.log | tail -20
```
Look for these lines to confirm a successful connection:
```
Connecting to telegram...
[Telegram] Auto-discovered Telegram fallback IPs: 149.154.166.110
[Telegram] set_my_commands OK for scope BotCommandScopeDefault (30 cmds)
[Telegram] Connected to Telegram (polling mode)
✓ telegram connected
```
If you see `Connected to Telegram (polling mode)` and `✓ telegram connected`, the bot is live.
For a full reference of restart commands, log signatures, curl test-message patterns, and sed snippets, see [`references/gateway-restart-and-test.md`](references/gateway-restart-and-test.md).
#### Sending a Test Message from the CLI
The Hermes `send_message` tool only works in sessions initiated from a messaging platform (Telegram, Discord, etc.). In a CLI session, it returns `"No messaging platforms connected"` even when the gateway has Telegram active.
**Workaround — use the Telegram Bot API directly via curl:**
```bash
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "text=Hello from Hermes! 👋"
```
The `${TELEGRAM_BOT_TOKEN}` is the value set in `.env` and `${CHAT_ID}` is the numeric user/chat ID. This sends a message bypassing the Hermes gateway — useful for testing or one-off notifications from the CLI.
## Gateway Check
To see if the gateway is running before troubleshooting:
```bash
ps aux | grep 'hermes gateway run'
```
## Key Configuration Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `TELEGRAM_BOT_TOKEN` | Yes | Bot token from @BotFather |
| `TELEGRAM_ALLOWED_USERS` | Recommended | Comma-separated numeric user IDs allowed to chat. **Leave empty to allow anyone** (not recommended for production). |
| `TELEGRAM_HOME_CHANNEL` | For cron | Default chat ID for cron job deliveries |
| `TELEGRAM_HOME_CHANNEL_NAME` | No | Display name for the home channel |
| `TELEGRAM_CRON_THREAD_ID` | No | Forum topic ID for cron deliveries in topic-mode groups |
## Long-Running Tasks & Blocking Behavior
The gateway processes **one turn at a time per session**. While the agent is inside a turn (thinking, running tools, waiting for a terminal command), it cannot receive or process new messages from you. Telegram shows "typing..." the entire time.
### Why the bot blocks
| Phase | What's happening | Can you message? |
|-------|-----------------|-----------------|
| 🔄 Agent calls a tool (e.g. `terminal()`) | Blocks waiting for result | ❌ Queued |
| 🤖 Agent calls the LLM again with tool output | Generating next action | ❌ Queued |
| 📨 Agent sends you a progress update | Brief mid-turn message | ❌ Still in same turn |
| ✅ Agent sends final response and ends turn | **Done** | ✅ Next message processed |
### "Typing..." = active LLM calls = cost
Every iteration of the agent loop makes a new API call to your LLM provider. While the bot shows "typing...", it is actively making LLM calls — you are being charged for input + output tokens each round. Input tokens grow each iteration as tool results are appended to the conversation context.
### How to avoid blocking the bot
Three approaches, in order of preference:
| Approach | How | Best for |
|----------|-----|----------|
| ⏰ **Cron job** | `cronjob(action='create', schedule='...', prompt='run my script', no_agent=True)` — script runs independently, stdout delivered verbatim to Telegram | Standalone scripts that run on a schedule or fire-and-forget |
| 🏃 **Background terminal** | `terminal(command='python long_script.py', background=true, notify_on_complete=true)` — agent returns immediately, pings you when done | One-off long scripts the user triggers |
| 📋 **/queue** | In CLI: `/queue <prompt>` — queues work for after current turn ends | Quick follow-ups during an active session |
The cron approach is cleanest: the bot never blocks, and `no_agent=True` means zero LLM calls during execution — the script's stdout is delivered verbatim.
### Verifying the bot is stuck mid-turn
Check the gateway logs for the last activity:
```bash
tail -20 ~/.hermes/logs/gateway.log
```
Look for:
- `inbound message: ... msg='your prompt'` — last message received
- `Flushing text batch ... (N chars)` — last update sent (but turn not over)
- No new `inbound message` entries — session is blocked, new messages are queued
## Pitfalls
- **Usernames are not IDs.** `TELEGRAM_ALLOWED_USERS` and `TELEGRAM_HOME_CHANNEL` need **numeric** Telegram IDs, not `@username` strings. Use @userinfobot to get them.
- **All vars start commented out.** The `.env` template ships with `#` prefix on every Telegram line. You must uncomment them.
- **Multi-variable line trap.** The default `.env` template writes `TELEGRAM_BOT_TOKEN` and `TELEGRAM_ALLOWED_USERS` on the same line: `# TELEGRAM_BOT_TOKEN=*** TELEGRAM_ALLOWED_USERS=`. This is NOT valid env format with the token value — split into two separate lines when configuring.
- **Gateway restart required.** `.env` is read at process startup. Simply running `hermes` in a new CLI session does not reload the gateway's env vars. Use `hermes gateway restart` (or `systemctl --user restart hermes-gateway`) every time you change `TELEGRAM_*` variables.
- **Config schema warnings can block clean startup.** If gateway status/logs show `custom_providers is a dict — it must be a YAML list`, fix it in `~/.hermes/config.yaml` by making `custom_providers` a list (or clear it with `hermes config set custom_providers "[]"`), then restart the gateway.
- **Token secrecy.** The bot token is a secret — anyone with it can control your bot. Keep it out of version control and shell history.
- **Gateway API server must be running.** The gateway also starts the API server (typically port 8642). If tools or web UI can't reach the agent, the root cause is often a stopped gateway, not Telegram itself.
- **send_message tool is CLI-blind to Telegram.** From a CLI session, `send_message(action=list)` returns `No messaging platforms connected` even when the gateway has Telegram active. This is by design — the tool only routes through gateway-originated sessions. Use the Telegram Bot API directly via curl to send messages from CLI sessions (see the Verification section for the exact curl command).
### Bot Commands: Registration & Limits
The gateway auto-registers bot commands from `hermes_cli.commands.telegram_bot_commands()` on startup. Commands come from three sources: built-in CommandDef entries, plugin slash commands, and skill entries. Gateway caps the command list at `MAX_COMMANDS_PER_SCOPE = 30` (telegram.py:108) to stay under Telegram's undocumented ~4KB payload limit.
**Adding a custom command persistently:**
1. Edit `~/.hermes/hermes-agent/hermes_cli/commands.py` -- add your (name, description) pair at the end of `telegram_bot_commands()`:
```python
result.append(("imagine", "Generate an image from a text prompt"))
```
2. Add the command to `_TELEGRAM_MENU_PRIORITY` if the cap is tight -- commands outside the priority list get trimmed first.
3. If you go over 30 commands, raise `MAX_COMMANDS_PER_SCOPE` in `telegram.py`.
4. Restart the gateway to pick up changes.
Without these three changes, custom commands get overwritten on gateway restart or silently truncated at the 30-command cap.
## Troubleshooting
### Diagnostic Flow: Bot Not Responding
Start with a **status check first** — it's the fastest way to narrow the cause.
```
hermes gateway status # Primary check
```
If that's unavailable, fall back to:
```
ps aux | grep 'hermes gateway run' # Second — gateway process exists?
systemctl --user status hermes-gateway 2>/dev/null # Third — systemd service?
```
#### Branch A: Gateway is not running
The most common cause of "stopped working" is the gateway having been shut down. Check the logs to find out why:
```
grep -i "telegram\|error\|fail\|warn\|sigterm\|sigkill\|oom" ~/.hermes/logs/gateway.log | tail -20
```
Common shutdown signatures in the logs:
| Log pattern | Likely cause | Action |
|-------------|-------------|--------|
| `WARNING ... Shutdown context: signal=SIGTERM under_systemd=yes parent_pid=1` | systemd sent SIGTERM (user logout, reboot, service stop) | Restart gateway or install as permanent service |
| `ERROR ... signal=SIGKILL` | OOM killer or forced kill | Check memory pressure, `dmesg | grep -i oom` |
| `ERROR ... Traceback (most recent call last)` | Crash / unhandled exception | Read the traceback, fix the root cause |
| Logs end abruptly with no shutdown message | Process was killed externally (shell session closed, SSH disconnect) | Restart gateway |
| No Telegram-related log entries at all | Telegram may not be configured or enabled in config | Check `.env` for `TELEGRAM_*` vars, check `config.yaml` → `telegram` section |
If the log shows the gateway was working fine then received SIGTERM, the fix is straightforward — restart it:
```bash
hermes gateway run # foreground (session-dependent)
hermes gateway install # as permanent systemd user service (auto-restarts)
```
If no systemd service exists (`systemctl --user list-units --type=service --all | grep hermes` returns empty), the gateway was started manually and won't survive a logout/reboot without the service.
#### Branch B: Gateway is running but not responding
```
grep -i telegram ~/.hermes/logs/gateway.log | tail -30
```
Look for:
- `[Telegram] Connected to Telegram (polling mode)` — connection was established
- `inbound message: platform=telegram user=...` — messages are being received
- `Sending response (... chars) to 1498679692` — responses are being sent
- `ERROR` or `WARNING` entries — something went wrong
If messages arrive but responses fail, check:
1. `TELEGRAM_BOT_TOKEN` is correct and uncommented in `.env`
2. The bot hasn't been blocked by Telegram (try sending a test from a different bot)
3. Network/firewall: Telegram polling needs outbound connectivity to `api.telegram.org`
#### Branch C: Never worked (first-time setup)
If the user says the bot never responded:
1. Follow the full Setup Steps section above
2. Check that `TELEGRAM_BOT_TOKEN` is on its own line (not merged with another var)
3. Verify with `curl` directly: `curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getMe"` — should return `{"ok":true,"result":{"id":...,"is_bot":true,"first_name":"...","username":"..."}}`
4. Check `TELEGRAM_ALLOWED_USERS` — without it, the bot defaults to allowing everyone; if it's set to the wrong numeric ID, the bot ignores your messages
5. After fixing, restart the gateway
6. Wait 5-10 seconds, then send a message to the bot on Telegram
### Symptom: "Bot responds to everyone / ignores me"
Check `TELEGRAM_ALLOWED_USERS` is set to your numeric user ID. Without this, the bot is open to anyone who finds it.
### Symptom: "Cron messages don't arrive on Telegram"
1. Ensure `TELEGRAM_HOME_CHANNEL` is set to the correct numeric chat ID
2. The home channel is where cron delivers by default — if you DM'd the bot manually but set the home channel to a group, cron goes to the group, not your DMs
### Symptom: "send_message tool says 'No messaging platforms connected' from CLI"
This is expected behavior — the `send_message` tool only routes through a session that was initiated from that platform. From a CLI session, it cannot discover Telegram even if the gateway has it connected.
**Workaround:** Call the Telegram Bot API directly:
```bash
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "text=Your message"
```
### Symptom: "Can't restart — no systemd service found"
If `systemctl --user list-units` shows no hermes service, the gateway was started manually (e.g., via `hermes gateway run` in a background terminal). See the **Manual Restart (no systemd service)** section in step 4 above.
@@ -0,0 +1,105 @@
# Gateway Restart & Test Message Reference
## Checking Gateway Status
```
ps aux | grep 'hermes gateway run'
```
Note the PID and whether it's running as Ssl (stable).
## Checking for systemd Service
```
systemctl --user list-units --type=service --all | grep -i hermes
```
Empty response = gateway was started manually, not systemd-managed.
## Manual Restart Sequence (no systemd)
```
# 1. Find and kill old gateway
ps aux | grep 'hermes gateway run'
kill -9 <PID> # -9 if plain kill doesn't work
sleep 2
# 2. Verify it's dead
ps aux | grep 'hermes gateway run'
# 3. Start fresh
# In the Hermes CLI, use:
# terminal(background=true, notify_on_complete=true, command="hermes gateway run")
# Do NOT use nohup, disown, or trailing & in foreground terminal()
```
## Verifying Telegram Connection in Logs
Log file: `~/.hermes/logs/gateway.log`
### Lines confirming a successful connection:
```
Connecting to telegram...
[Telegram] Auto-discovered Telegram fallback IPs: 149.154.166.110
[Telegram] set_my_commands OK for scope BotCommandScopeDefault (30 cmds)
[Telegram] Connected to Telegram (polling mode)
✓ telegram connected
```
### Quick grep check:
```
grep -E "(telegram connected|Connecting to telegram|Connected to Telegram)" ~/.hermes/logs/gateway.log
```
## Sending a Test Message (CLI Workaround)
The `send_message` Hermes tool does NOT work from CLI sessions for Telegram — it returns "No messaging platforms connected". Use the Telegram Bot API directly via curl:
```
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "text=Hello from Hermes!"
```
### Expected response on success:
```json
{"ok":true,"result":{"message_id":677,"from":{"id":8971430276,"is_bot":true,...},"chat":{"id":1498679692,...},"date":1780001714,"text":"Hello from Hermes!"}}
```
If `"ok":true`, the message was delivered. You can also add `-d "parse_mode=HTML"` for rich text formatting.
## .env Configuration Snippets
### Default state (all commented out):
```
# TELEGRAM_BOT_TOKEN=*** TELEGRAM_ALLOWED_USERS=
# TELEGRAM_HOME_CHANNEL=
# TELEGRAM_HOME_CHANNEL_NAME=
```
### After configuration — split into separate lines:
```env
TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_ALLOWED_USERS=123456789
TELEGRAM_HOME_CHANNEL=123456789
TELEGRAM_HOME_CHANNEL_NAME=@yourusername
```
### sed commands to uncomment (one per line):
```
sed -i 's|^# TELEGRAM_ALLOWED_USERS=.*|TELEGRAM_ALLOWED_USERS=123456789|' ~/.hermes/.env
sed -i 's|^# TELEGRAM_HOME_CHANNEL=.*|TELEGRAM_HOME_CHANNEL=123456789|' ~/.hermes/.env
sed -i 's|^# TELEGRAM_HOME_CHANNEL_NAME=.*|TELEGRAM_HOME_CHANNEL_NAME=@username|' ~/.hermes/.env
```
**Caveat on TELEGRAM_BOT_TOKEN:** The default template has `TELEGRAM_BOT_TOKEN` and `TELEGRAM_ALLOWED_USERS` on the same commented line. The simple `sed` to uncomment probably won't work cleanly. Verify the exact line format with `grep -n 'TELEGRAM_BOT_TOKEN' ~/.hermes/.env` and replace the entire line if needed, or delete the old line and insert fresh ones.
## Token Visibility Note
In gateway logs and grep output, the bot token may appear partially masked (e.g., `TELEGRAM_BOT_TOKEN=***...***`). This is normal log sanitization — the full token is still loaded and used by the gateway process.
@@ -0,0 +1,200 @@
---
name: hermes-webui
description: Deploy, configure, and troubleshoot the Hermes Web UI Docker container — the browser interface for Hermes Agent.
version: 1.0.0
author: Hermes Agent
license: MIT
platforms: [linux, macos]
metadata:
hermes:
tags: [hermes, webui, docker, browser, troubleshooting]
source: https://github.com/nesquena/hermes-webui
related_skills: [hermes-agent, hermes-dashboard]
---
# Hermes Web UI
The Hermes Web UI (`ghcr.io/nesquena/hermes-webui`) provides a browser-based interface for Hermes Agent — chat with the agent, browse sessions, manage workspace files, and monitor agent activity. It runs as a Docker container alongside the Hermes Agent gateway.
For a **management-focused control panel** (config editing, model switching, tool toggling, cron control, log viewing), see the `hermes-dashboard` skill — a complementary Flask dashboard that reads Hermes state directly from the filesystem.
## Quick Start
```bash
# Pull and run
docker run -d \
--name hermes-webui \
-p 8787:8787 \
-v /path/to/hermes-home:/home/hermeswebui/.hermes \
-v /path/to/workspace:/workspace \
-e HERMES_WEBUI_PASSWORD=your-password \
ghcr.io/nesquena/hermes-webui:latest
# With agent source mounted (for full functionality):
docker run -d \
--name hermes-webui \
-p 8787:8787 \
-v /path/to/hermes-home:/home/hermeswebui/.hermes:ro \
-v /path/to/hermes-agent-source:/home/hermeswebui/.hermes/hermes-agent:ro \
-v /path/to/workspace:/workspace \
-e HERMES_WEBUI_PASSWORD=your-password \
ghcr.io/nesquena/hermes-webui:latest
```
Open `http://localhost:8787` in a browser and log in with the password.
## Architecture
The Web UI requires two adjacent systems to function fully:
1. **Hermes Agent source code** — the Web UI imports `AIAgent` from the Hermes Agent Python library directly. Without it, features like model auto-detection, personality routing, and CLI session imports are disabled.
2. **Hermes Gateway / API Server** — the Web UI communicates with the Hermes API server (OpenAI-compatible, typically port 8642) for agent execution. The API server is started by the Hermes gateway process.
## Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `HERMES_WEBUI_PASSWORD` | (required) | Login password for the web interface |
| `HERMES_WEBUI_BIND_HOST` | `0.0.0.0` | IP to bind the HTTP server |
| `HERMES_WEBUI_BIND_PORT` | `8787` | Port for the HTTP server |
| `HERMES_WEBUI_STATE_DIR` | `~/.hermes/webui` | Where sessions, workspaces, and state are stored |
| `HERMES_WEBUI_DEFAULT_WORKSPACE` | `/workspace` | Default workspace directory shown on first launch |
| `WANTED_UID` | `1024` | User ID to run as (auto-detected from mounted volumes) |
| `WANTED_GID` | `1024` | Group ID (auto-detected from mounted volumes) |
### Volume Mounts
| Host Path | Container Path | Purpose |
|-----------|---------------|---------|
| `~/.hermes` | `/home/hermeswebui/.hermes` | Hermes home directory (config, sessions, skills) |
| `~/.hermes/hermes-agent` | `/home/hermeswebui/.hermes/hermes-agent` | Agent source code (for AIAgent import) |
| `/path/to/workspace` | `/workspace` | Workspace/project files |
## Common Tasks
### Check if agent is recognized
```bash
docker logs hermes-webui | grep -E "agent dir|AIAgent"
```
Expected healthy output:
```
agent dir : /home/hermeswebui/.hermes/hermes-agent [ok]
```
### Verify health
```bash
curl -s http://localhost:8787/health
```
### View server logs
```bash
docker logs hermes-webui
```
### Restart
```bash
docker restart hermes-webui
```
## Pitfalls
- **`HERMES_WEBUI_STATE_DIR` is required.** Despite having a default in the docs, the container errors out hard without it: `!! ERROR: HERMES_WEBUI_STATE_DIR not set`. Always pass `-e HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui` on the docker run command, or the container will crash-loop.
- **Agent source must be accessible at container startup.** The init script installs dependencies from `pyproject.toml` before the server starts. If the source is added after the container is already running, you must remove `/app/venv/.deps_installed` and restart for the init script to reinstall.
- **Read-only mounts recommended.** The init script warns if the agent source mount is writable from the WebUI container. The multi-container compose defaults use a read-only mount for defence-in-depth.
- **Password redaction in terminal output.** When constructing the `docker run` command through the terminal tool, the password in `-e HERMES_WEBUI_PASSWORD=secret` may be replaced with `***` by secret redaction before the command executes. To bypass: use `execute_code` with Python to construct the command, or write the password hex-encoded and decode it in the heredoc, or verify the deployed password by checking its length and first/last chars via `docker inspect`.
- **Permission errors on restart.** Prior `pip install -e` runs as root may leave root-owned `.pyc` files in the venv. If the init script errors with `Permission denied` during reinstall, clean them: `docker exec hermes-webui find /app/venv -user root -delete`
- **First startup is slow.** The init script installs dependencies on first run — allow ~30 seconds for startup.
- **API server must be running.** The Web UI needs the Hermes API server (started by `hermes gateway run`) on port 8642. Without it, agent chat will fail even if the Web UI's `health` endpoint reports OK.
- **Provider credential mismatch.** The Web UI has its OWN isolated `.env` and `config.yaml` inside the mounted Hermes home directory, separate from the host's Hermes config. If these files use a different provider or a placeholder API key, the agent will fail with HTTP 401 errors. Always sync both files when switching providers. See `references/provider-credentials-and-sessions.md`.
- **Stale sessions retain old provider settings.** Sessions are cached in `<state_dir>/sessions/<id>.json` with the model and provider that were active at creation time. Switching providers in config.yaml does NOT update existing sessions. The web UI reads `s.model` and `s.model_provider` from the cached session and passes them to `AIAgent.__init__`, which then tries the OLD provider. Symptoms: "No LLM provider configured" on messages even after a correct provider config. Fix: update both `model` and `model_provider` fields in the session JSON, or delete the session file so the frontend creates a fresh one.
- **settings.json caches default_model_provider.** The file at `<state_dir>/settings.json` stores `default_model_provider`. If you switched providers, update this field too — otherwise new sessions will still be created with the old provider.
## Troubleshooting
### Symptom: HTTP 401 on chat
Check the web UI's `.env` has the correct API key for the provider in `config.yaml`:
```bash
# Compare config provider with available keys
docker exec hermes-webui cat /home/hermeswebui/.hermes/config.yaml
docker exec hermes-webui cat /home/hermeswebui/.hermes/.env
```
The key variable name must match what the provider expects (e.g. `DEEPSEEK_API_KEY` for `deepseek`, `OPENROUTER_API_KEY` for `openrouter`).
### Symptom: "No LLM provider configured" on every message
This usually means the cached session has a stale provider. Check and fix:
```bash
# 1. Check what model/provider the session has
docker exec hermes-webui bash -c 'python3 -c "
import json
with open(\"/home/hermeswebui/.hermes/webui/sessions/$(
ls -t /home/hermeswebui/.hermes/webui/sessions/*.json 2>/dev/null | head -1
)\") as f:
d = json.load(f)
print(f\"model={d.get(\\\"model\\\")} provider={d.get(\\\"model_provider\\\")}\")
"'
# 2. Fix the session (replace with your actual provider/model)
docker exec hermes-webui bash -c 'python3 -c "
import json
p = \"/home/hermeswebui/.hermes/webui/sessions/*.json\"
import glob
for f in glob.glob(p):
with open(f) as fh:
d = json.load(fh)
d[\"model\"] = \"deepseek-v4-flash\"
d[\"model_provider\"] = \"deepseek\"
with open(f, \"w\") as fh:
json.dump(d, fh)
"'
# 3. Also fix settings.json if needed
docker exec hermes-webui bash -c 'python3 -c "
import json
with open(\"/home/hermeswebui/.hermes/webui/settings.json\") as f:
d = json.load(f)
d[\"default_model_provider\"] = \"deepseek\"
with open(\"/home/hermeswebui/.hermes/webui/settings.json\", \"w\") as f:
json.dump(d, f, indent=2)
"'
# 4. Restart to pick up changes
docker restart hermes-webui
```
Alternatively, delete the stale session entirely and let the frontend create a fresh one:
```bash
docker exec hermes-webui bash -c 'rm -f /home/hermeswebui/.hermes/webui/sessions/*.json && echo "{}" > /home/hermeswebui/.hermes/webui/sessions/_index.json'
docker restart hermes-webui
```
### Symptom: Provider shows as configured but models fail to load
Check that the web UI's config.yaml uses the correct key names for the model section:
```yaml
model:
provider: deepseek # Must match a provider in the Hermes registry
default: deepseek-v4-flash # The model name
base_url: https://api.deepseek.com/v1
```
The config.yaml key is `default` (not `model`). If you wrote `model:` inside the `model:` section, provider resolution will return empty.
## References
- `references/aiagent-not-available.md` — fixing the "AIAgent not available" error when the container cannot find the Hermes Agent source.
- `references/provider-credentials-and-sessions.md` — detailed diagnosis flow for 401 errors and stale session/provider mismatches, including scripted fixes.
- `references/firecrawl-reddit-limitations.md` — Firecrawl explicitly blocks Reddit; only WSB RSS works.
@@ -0,0 +1,104 @@
# Fixing "AIAgent not available" in Hermes Web UI
The Web UI cannot find the Hermes Agent source code and starts in reduced-functionality mode. Symptoms in `docker logs hermes-webui`:
```
!! WARNING: hermes-agent source not found.
!! Looked in: /home/hermeswebui/.hermes/hermes-agent
ImportError: AIAgent not available -- check that hermes-agent is on sys.path
agent dir : NOT FOUND [XX]
```
## Root Cause
The container needs the Hermes Agent Python source at `/home/hermeswebui/.hermes/hermes-agent` (or `/opt/hermes` as fallback). This is used by the init script to:
1. Install dependencies from the agent's `pyproject.toml`
2. Make `AIAgent` importable from `run_agent.py`
When the source is missing, the server starts without any agent features.
## Fix (agent source is on host but not in the container)
If your Hermes home is bind-mounted and the agent source exists on the host but wasn't mounted:
### Option A: Copy source into the mounted data directory (no restart needed)
```bash
# Copy host's hermes-agent source into the directory already mounted to the container
cp -r /path/to/host/.hermes/hermes-agent /path/to/mounted-data/hermes-agent
# Install deps inside the container
docker exec hermes-webui bash -c "source /app/venv/bin/activate && pip install -e /home/hermeswebui/.hermes/hermes-agent"
# Remove the fast-restart marker and restart to force full setup
docker exec hermes-webui rm -f /app/venv/.deps_installed
docker restart hermes-webui
# If permission errors occur (root-owned pycache from prior pip install):
docker exec hermes-webui bash -c "find /app/venv -user root -delete"
docker restart hermes-webui
```
### Option B: Add a proper bind mount (cleaner, Docker Compose)
Add to the webui service in your docker-compose.yml:
```yaml
services:
hermes-webui:
image: ghcr.io/nesquena/hermes-webui:latest
volumes:
- ~/.hermes:/home/hermeswebui/.hermes:ro
- ~/.hermes/hermes-agent:/home/hermeswebui/.hermes/hermes-agent:ro # <-- add this
- /path/to/workspace:/workspace
environment:
- HERMES_WEBUI_PASSWORD=***
ports:
- "8787:8787"
```
Then recreate:
```bash
docker compose up -d
```
## Verifying the fix
```bash
# Check startup logs for success
docker logs hermes-webui | grep "agent dir"
# Expected:
# agent dir : /home/hermeswebui/.hermes/hermes-agent [ok]
# Also confirm no AIAgent import errors:
docker logs hermes-webui | grep -i "AIAgent"
# Should return nothing (no errors)
```
## Known edge cases
### Fast restart skipping agent install
The init script checks for `/app/venv/.deps_installed`. If the agent source was added after the container's first startup (when the marker was created), the init script skips reinstalling and the import still fails even though the source is present.
**Fix:** Remove the marker and restart:
```bash
docker exec hermes-webui rm -f /app/venv/.deps_installed
docker restart hermes-webui
```
### Permission denied during reinstall ("os error 13")
A prior `pip install -e` or manual install running as root leaves root-owned `.pyc` files in `/app/venv/lib/python3.12/site-packages/__pycache__/`. When the init script later runs as a non-root user, it can't remove them.
**Fix:** Clean root files from the venv:
```bash
docker exec hermes-webui bash -c "find /app/venv -user root -delete"
docker restart hermes-webui
```
@@ -0,0 +1,9 @@
# Firecrawl + Reddit Limitations
Firecrawl (firecrawl.dev, `firecrawl-py` SDK) explicitly blocks Reddit:
```
HTTP 403: "We apologize for the inconvenience but we do not support this site."
```
Applies to all Reddit URLs. See `devops/hermes-dashboard/references/firecrawl-reddit-limitations.md` for full details including the WSB RSS workaround.
@@ -0,0 +1,146 @@
# Provider Credentials & Session State Diagnosis
Diagnosis flow for HTTP 401 errors and "No LLM provider configured" when the web UI has the agent source installed (agent dir shows `[ok]` in logs).
## Diagnosis Flow
### Step 1 — Check what provider the web UI is actually trying to use
```bash
docker exec hermes-webui cat /home/hermeswebui/.hermes/config.yaml
```
The `model.provider` field is the provider the web UI will attempt to use. If this says `openrouter` but your API keys are for `deepseek`, that's the problem.
### Step 2 — Check that the .env has the right API key
```bash
docker exec hermes-webui cat /home/hermeswebui/.hermes/.env
```
The env var name must match what the provider expects. Common mappings:
| provider in config.yaml | env var |
|---|---|
| `deepseek` | `DEEPSEEK_API_KEY` |
| `openrouter` | `OPENROUTER_API_KEY` |
| `anthropic` | `ANTHROPIC_API_KEY` |
| `openai` / `openai-api` | `OPENAI_API_KEY` |
| `gemini` | `GOOGLE_API_KEY` or `GEMINI_API_KEY` |
### Step 3 — Check if the error is from a stale cached session
The web UI caches session metadata (model, provider, workspace) on disk. When you switch providers, old sessions keep the old provider.
```bash
# Find the most recent session file
ls -t /home/hermeswebui/.hermes/webui/sessions/*.json 2>/dev/null | head -3
# Check its model/provider
python3 -c "
import json, glob
for f in sorted(glob.glob('/home/hermeswebui/.hermes/webui/sessions/*.json')):
with open(f) as fh:
d = json.load(fh)
print(f'{f}: model={d.get(\"model\")} provider={d.get(\"model_provider\")}')
"
```
Expected: `model=deepseek-v4-flash provider=deepseek` (or whatever your current provider is).
If the session shows a different provider, update it:
```bash
python3 -c "
import json, glob
for f in glob.glob('/home/hermeswebui/.hermes/webui/sessions/*.json'):
with open(f) as fh:
d = json.load(fh)
d['model'] = 'deepseek-v4-flash' # <-- replace with your model
d['model_provider'] = 'deepseek' # <-- replace with your provider
with open(f, 'w') as fh:
json.dump(d, fh)
"
```
### Step 4 — Check settings.json
The web UI stores `default_model_provider` in settings.json:
```bash
python3 -c "
import json
with open('/home/hermeswebui/.hermes/webui/settings.json') as f:
d = json.load(f)
print('default_model_provider:', d.get('default_model_provider'))
"
```
If this doesn't match your current provider:
```bash
python3 -c "
import json
with open('/home/hermeswebui/.hermes/webui/settings.json') as f:
d = json.load(f)
d['default_model_provider'] = 'deepseek' # <-- replace with your provider
with open('/home/hermeswebui/.hermes/webui/settings.json', 'w') as f:
json.dump(d, f, indent=2)
"
```
### Step 5 — Verify provider resolution works
Test what the web UI resolves at runtime:
```bash
python3 -c "
import os
os.environ['HERMES_HOME'] = '/home/hermeswebui/.hermes'
from api.config import get_config, resolve_model_provider, model_with_provider_context
cfg = get_config()
print('Config model section:', cfg.get('model', {}))
# Simulate sending a message without specifying a model
model_with_ctx = model_with_provider_context('')
resolved = resolve_model_provider(model_with_ctx)
print('Resolved (empty model):', resolved)
# Simulate sending a message with your default model
model_with_ctx = model_with_provider_context('deepseek-v4-flash')
resolved = resolve_model_provider(model_with_ctx)
print('Resolved (default model):', resolved)
# Check runtime provider can find the API key
from api.oauth import resolve_runtime_provider_with_anthropic_env_lock
from hermes_cli.runtime_provider import resolve_runtime_provider
_rt = resolve_runtime_provider_with_anthropic_env_lock(
resolve_runtime_provider,
requested=resolved[1], # the resolved provider name
)
print('API key found:', bool(_rt.get('api_key')))
"
```
If the key is found and provider/base_url are correct, the config is right and the issue is in the session cache.
## Common Patterns
### Pattern: OpenRouter → DeepSeek switch
After switching from OpenRouter to DeepSeek, three things need updating:
1. `config.yaml` — set `model.provider: deepseek`, `model.default: deepseek-v4-flash`, `model.base_url: https://api.deepseek.com/v1`
2. `.env` — set `DEEPSEEK_API_KEY=<your-key>`
3. Session cache — update session JSON files to use `model_provider: deepseek` instead of `openrouter`
4. `settings.json` — update `default_model_provider` to `deepseek`
### Pattern: Copying host config to web UI
The host's `~/.hermes/` is NOT mounted into the web UI container. The web UI uses whatever is in the bind-mounted data directory (e.g. `/home/ray/docker/hermes/data/`). To sync:
```bash
cp ~/.hermes/config.yaml /path/to/webui/data/config.yaml
grep -E '^(DEEPSEEK|OPENROUTER|ANTHROPIC|OPENAI|GEMINI|GOOGLE)_API_KEY' ~/.hermes/.env >> /path/to/webui/data/.env
```