Adding a New Agent

Files touched

FilePurpose
src/agents.rsAgent registry entry (name, binary, detection, flags)
src/tmux/status_detection.rsStatus detection function (pane parsing or stub)
src/hooks/mod.rsHook installer (if the agent supports hooks)
src/session/instance.rsWire hook installation + AOE_INSTANCE_ID env prefix
src/session/container_config.rsConfig mount for Docker sandbox
src/acp/agent_registry.rsStructured view ACP adapter entry (only if the agent ships an ACP server)
src/acp/agent_profiles.rs + web/src/lib/agentProfiles.tsStructured view profile (clear aliases, meta namespace, capability gates, tool aliases)
src/acp/install_hints.rsInstall hint surfaced by aoe acp doctor and handshake failures
docker/DockerfileInstall agent in sandbox image
docs/structured-view.mdPer-agent structured view feature matrix
README.md, docs/Documentation updates

Levels of support

Each level is additive; do only what the agent supports.

LevelWhat it givesRequires
1. BasicAppears in aoe agents, sessions launch, status always “Idle”AgentDef + stub detect_status
2. Pane-parse statusStatus inferred from terminal output; no agent config, brittle to UI changesdetect_<agent>_status(&str) -> Status (OpenCode, Vibe, Copilot, Pi, Droid)
3. Hook statusAgent writes status to a file via hooks; reliable, survives UI changeshook_config + generic install_hooks() or a custom install_<agent>_hooks() (Claude, Cursor, Gemini generic; Codex TOML, Hermes YAML, Kiro JSON)
4. Session resumeRestart resumes the prior conversationresume_strategy in AgentDef
5. Docker sandboxRuns isolated; host config synced inAgentConfigMount + Dockerfile install

Steps

1. Research: binary name, detection (which), YOLO/auto-approve flag, resume flag, hook support + format (JSON/YAML/TOML), config dir, install command.

2. AgentDef (src/agents.rs): add to the AGENTS array. Key fields: detection: DetectionMethod::Which(...), yolo: Some(YoloMode::CliFlag(...)), hook_config (Some(...) for Claude-format hooks), resume_strategy, host_only, install_hint. set_default_command: true only when the binary name alone isn’t enough to relaunch (e.g. opencode).

3. Status detection (src/tmux/status_detection.rs): hook-based agents get a stub returning Status::Idle. Pane-parse agents get a function matching on lowercased pane content. Prefer --format json over substring matching when the CLI offers it; human-readable output changes between versions.

4. Hooks (if applicable): for non-Claude formats add a custom installer in src/hooks/mod.rs (see install_hermes_hooks, install_kiro_hooks). Wire it into install_agent_status_hooks() in src/session/instance.rs, and add the tool name to status_hook_env_prefix() so AOE_INSTANCE_ID reaches the hook (without it hooks write nothing). Keep installers as pure file IO; any subprocess work (e.g. setting a default agent) goes in a separate function so cargo test doesn’t mutate the dev’s real environment.

5. Container mount (src/session/container_config.rs): add an AgentConfigMount (tool_name, host_rel, container_suffix, skip_entries). Host hook installation does not cover sandbox sessions; if the agent uses hooks, wire them into build_container_config so the sidecar volume mounts and config materializes in the container.

6. Dockerfile (docker/Dockerfile): install the agent and add its config dir to the mkdir -p block.

7. Tests: update the src/agents.rs tests (test_get_agent_known, test_agent_names, test_resolve_tool_name, test_settings_index_roundtrip, test_send_keys_enter_delay, test_install_hint_lookup); add a detection test in status_detection.rs; for hook-based agents add to test_status_hook_env_prefix_includes_hermes.

8. Structured view profile (if the agent ships an ACP server): its CLI accepts acp/--acp or ships a *-acp adapter. Add the binary to src/acp/agent_registry.rs::with_defaults() (keyed on the src/agents.rs name), an install hint to src/acp/install_hints.rs, a server profile to src/acp/agent_profiles.rs (registered in resolve()), and a mirrored profile in web/src/lib/agentProfiles.ts (registered in PROFILES). Keep profiles conservative: until you’ve observed the adapter’s _meta convention for child tool-call linkage, leave parent_meta_namespaces and the alias map empty. Missing indentation is safer than fake parent links; an empty alias map renders the generic tool card, which is the correct fallback. Add the agent to the feature matrix in docs/structured-view.md; profile mechanics are documented in docs/development/internals/structured-view.md.

9. Docs: README.md (features + FAQ), docs/index.md (supported agents), docs/guides/sandbox.md (image table), docker/Dockerfile.dev (inherited-agents comment).

10. Verify:

cargo fmt && cargo clippy -- -D warnings
cargo test --lib agents
cargo test --lib <youragent>
cargo test --lib container_config
cargo build && ./target/debug/aoe agents   # verify detection

Hook format reference

Claude/Cursor/Gemini (generic hook_config)

Set hook_config: Some(AgentHookConfig { ... }); the generic install_hooks() handles it.

{
  "hooks": {
    "PreToolUse": [{"hooks": [{"type": "command", "command": "sh -c '...'"}]}],
    "Stop": [{"hooks": [{"type": "command", "command": "sh -c '...'"}]}]
  }
}

Each entry in events: &[HookEvent] carries:

FieldMeaning
nameAgent’s event name (e.g. "PreToolUse").
matcherOptional pattern for events that need it (e.g. Claude’s Notification matcher).
statusSome("running"|"waiting"|"idle") to install a status-writer on this event, or None for a purely lifecycle event.
session_id_capturetrue installs a command that extracts session_id from the agent’s stdin JSON and writes it to /tmp/aoe-hooks/<AOE_INSTANCE_ID>/session_id, read by session-resume. Currently only Claude (SessionStart, UserPromptSubmit). With status also set, both commands share the matcher block and the session-id command runs first so it consumes stdin before the status writer.

Codex (custom TOML)

[hooks] table in .codex/config.toml:

[[hooks.PreToolUse]]
[[hooks.PreToolUse.hooks]]
type = "command"
command = "sh -c '...'"

Set hook_config: Some(AgentHookConfig { settings_rel_path: ".codex/config.toml", ... }). Host installs must go through install_codex_hooks() / uninstall_codex_hooks() so CODEX_HOME, existing [hooks.state] trust data, [features].hooks = false, the config.toml.lock, and atomic replacement are respected. Codex status is hook-first with targeted pane reconciliation for known hook gaps.

Hermes (custom YAML)

hooks:
  pre_tool_call:
    - command: "sh -c '...'"

Kiro CLI (custom JSON agent config)

{
  "name": "aoe-hooks",
  "tools": ["*"],
  "hooks": {
    "preToolUse": [{"command": "sh -c '...'"}],
    "stop": [{"command": "sh -c '...'"}]
  }
}

Common pitfalls

  • Missing status_hook_env_prefix: without AOE_INSTANCE_ID, hooks write nothing.
  • Wrong hook format: test that hooks fire by sending a message and checking /tmp/aoe-hooks/*/status.
  • Sandbox hooks are separate: host installation skips containers; wire into build_container_config too.
  • Waiting status needs a dedicated event: not all agents expose an approval/permission event. If none exists, document it as a limitation and consider filing upstream.