Adding a New Agent

Files touched

File Purpose
src/agents.rs Registry entry (name, binary, detection, flags)
src/tmux/detect/manifests/<agent>.toml Detection rules, for a pane-parsed agent
src/tmux/status_detection.rs Detection entry point (manifest call or stub)
src/hooks/mod.rs Hook installer, if the agent supports hooks
src/session/instance/hooks.rs Hook wiring plus the AOE_INSTANCE_ID env prefix
src/session/config/container_config.rs Config mount for the sandbox
src/acp/agent_registry.rs ACP adapter entry, if the agent ships an ACP server
src/acp/agent_profiles.rs, web/src/lib/agentProfiles.ts Structured view profile
src/acp/install_hints.rs Install hint for aoe acp doctor and handshake failures
docker/Dockerfile Install the agent in the sandbox image
docs/structured-view.md, docs/index.md, README.md Supported-agent lists

Levels of support

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

Level What it gives Requires
1. Basic Appears in aoe agents, sessions launch, status always Idle AgentDef plus a stub detect_status
2. Pane-parse status Status inferred from terminal output A manifest plus detect_<agent>_status calling into it
3. Hook status The agent writes status the instant it changes hook_config with the generic install_hooks(), or sidecar_hooks with a custom installer
4. Session resume A restart resumes the same native conversation A session_support contract with verified resume argv
5. Sandbox Runs isolated with host config synced in AgentConfigMount plus a Dockerfile install

Levels 3 and 4 are independent. session_support declares verified native resume argv; its optional capture spec is the only backend allowed to supply an id, with each environment explicitly PaneScoped, Preassigned, ManagedExclusiveStore, or Unsupported. Use capture: None for argv-only support, and omit session_support entirely when the resume argv itself is unverified. Managed-store capture additionally requires a physically per-instance store, a cwd match, a launch-time floor, and a cross-process ownership lease.

Steps

1. Research the binary name, detection, YOLO flag, exact resume and fork argv, authoritative session-id source, host and sandbox storage paths, hook identity field, config dir, and install command. Treat an unverified environment as unsupported.

2. AgentDef in src/agents.rs: add to AGENTS, declaring detection, YOLO mode, hooks, lifecycle, and SessionSupport where resume argv is verified. Add a capture spec only for an authoritative source, naming one backend plus separate host and sandbox contexts. Hook-based capture must declare HookIdentityField::SessionId or ConversationIdOrSessionId from the upstream payload contract.

Add a session_name_flag arm only from documented argv: it names the agent’s own session at launch (claude --name <title>) when session.name_agent_session is on.

3. Status detection: an agent whose pane carries state gets a manifest in src/tmux/detect/manifests/<agent>.toml and a detect_<agent>_status calling into it (see detect_claude). Rules are {id, state, priority, region, matcher} and the highest-priority match wins, so a new case is a row rather than another branch. The hook file is a rule too (region = "hook"), which is what lets a blocking prompt on screen outrank a running write. Mark a rule visible = true only when it reads state off the agent’s own live chrome, which is what lets the poller publish it without a confirming capture. Agents with no pane signal keep a stub returning Status::Idle.

Register the manifest with a static surface adapter in src/tmux/detect/mod.rs. Most agents use the plain adapter: unmodified rows and 50 lines of scrollback. An agent that needs surface preprocessing keeps its parsing, masking, capture policy, and semantic observation token in its private adapter module. For manifest-based detection, the poller and CLI use the same DetectionPlan without branching on agent names.

Session-ID pollers retain the configured thread-count budget and use standard Rust thread stacks. Agent capture can deserialize complete peer instances, including nested execution metadata.

Adapters supply borrowed Evidence slices with original bottom offsets. A manifest uses region = "evidence(name)"; names must be declared by its adapter and compile to indices, including nested clause overrides. Extract source spans before masking, preserve row positions, and preprocess before truncating common recent regions. Configured status rules bypass adapter preprocessing and use the plain capture policy; a configured no-match remains authoritative Idle.

Evidence may also borrow a diagnostic from the original capture. Extract only the message owned by that surface, preserving captured line breaks and rejecting ambiguous boundaries. Keep its matching text and bottom offset unchanged. Only a selected Error rule’s own evidence region can supply Detection::diagnostic; skipped updates, nested clauses, and other winners cannot inherit it. The poller uses this text when first recording an error, with the existing display-length limit, and otherwise keeps the pane-summary fallback.

4. Hooks: for non-Claude formats add an installer in src/hooks/mod.rs (see install_hermes_hooks_with_events), wire it into SidecarHooks::install, and add the agent to status_hook_env_prefix() so AOE_INSTANCE_ID and AOE_PROFILE reach the hook. Without the instance id, hooks write nothing. Use HookStatus rather than raw strings, and keep installers pure file IO: any subprocess work belongs in a separate function so cargo test cannot mutate a developer’s real environment.

5. Container mount: add an AgentConfigMount (tool_name, host_rel, container_suffix, skip_entries) so the resolved config store is mounted where the containerized binary reads it, and install hooks and session-id sidecars into that mounted store. Declare a sandbox capture context only after this path is proven.

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

7. Structured view profile, if the agent ships an ACP server (its CLI accepts acp / --acp, or it ships a *-acp adapter): add the binary to agent_registry.rs::with_defaults() keyed on the src/agents.rs name, an install hint, a server profile registered in resolve(), and a mirrored frontend profile in PROFILES. Keep profiles conservative: until you have observed the adapter’s _meta convention for child tool calls, leave parent_meta_namespaces and the alias map empty, since missing indentation is safer than fake parent links and an empty alias map renders the correct generic card.

8. Tests: update the registry matrix and settings round-trip tests, then cover resume argv, the capture backend, host and sandbox contexts, missing-id fail-closed behavior, /clear rotation where supported, restart persistence, and two concurrent sessions in the same cwd. Managed-store tests must prove the launch floor and ownership lease reject stale or peer-owned ids, and hook agents that the declared identity field reaches the pane-scoped sidecar.

9. 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

Hook format reference

Claude and Gemini (generic hook_config)

Set hook_config: Some(AgentHookConfig { ... }) and the generic install_hooks() handles their nested settings schema:

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

Each HookEvent carries:

Field Meaning
name The agent’s event name, e.g. "PreToolUse".
matcher Optional pattern, for events that need one.
status Some(HookStatus::…) installs a status writer on this event; None is a purely lifecycle event.
identity_field Installs a command that extracts the declared top-level native identity from stdin into the pane-scoped session_id sidecar. Use only a field documented upstream. With status also set, the identity command runs first, and it stays installed when agent_status_hooks = false.
waiting_tools Tool names whose invocation blocks on the user for the tool’s whole execution (Claude’s AskUserQuestion). The status writer then inspects the payload’s tool_name and writes waiting instead of the event’s status. Pair it with a tool-scoped event that restores the normal status, or the status sticks on waiting.

Other formats

  • Cursor Agent: version 1 .cursor/hooks.json, with direct command entries under hooks.beforeSubmitPrompt. Its stable identity is conversation_id; generation_id is turn-scoped and must not be captured. Use SidecarHooks with install_cursor_hooks_with_events.
  • Codex: the same generic JSON payload, written to hooks.json in Codex’s config dir (settings_rel_path: ".codex/hooks.json", format: HookFormat::CodexJson). install_codex_json_hooks() checks the adjacent config.toml feature opt-out first, and empty-event cleanup removes AoE’s entries while preserving user hooks. Sandbox installation refuses linked or unreadable config files rather than following them. Codex’s config.toml holds [hooks.state] trust data and [features].hooks; do not point settings_rel_path at it.
  • Hermes: custom YAML, hooks.pre_tool_call[].command.
  • Kiro CLI: a custom JSON agent config with hooks.preToolUse[].command.
  • Kimi Code: a flat [[hooks]] array in .kimi-code/config.toml, which also holds provider and oauth settings, so the installer rewrites only its own entries.

Common pitfalls

  • Missing status_hook_env_prefix: without AOE_INSTANCE_ID hooks write nothing. Test by sending a message and checking /tmp/aoe-hooks-$(id -u)/*/status on the host, or /tmp/aoe-hooks/*/status inside the sandbox.
  • Sandbox hooks are separate: host installation skips containers, so wire into build_container_config too.
  • Waiting status needs its own event: not every agent exposes an approval event. Document the gap and consider filing upstream.
  • Sidebar quick permission response: the TUI’s a / A action needs each agent’s exact keystroke sequence, not detection. Set AgentDef.permission_response only once you have confirmed by hand how the agent’s prompt is answered (bare digit, arrow plus Enter, no assumed trailing Enter). Set allow_always: None when the prompt offers no “don’t ask again” choice, and leave the whole field None until verified: the action then tells the user the agent is not supported yet.