docs/07-agents.md
07 — Agent Integration Strategy
Layered detection, highest-signal first. Explicit signals always override heuristics. Unknown apps remain fully working terminals.
Priority stack
- Native integration — agent exposes state over a local API the
adapter can poll/subscribe (rare today; e.g. OpenCode
serveHTTP, Codexapp-server). Best-effort where officially documented. - Agent hooks / semantic events — the primary mechanism for
Phase 2. Small shims installed into each agent's hook system call
signaltty hook-event --agent <kind> --event <name> --pane $SIGNALTTY_PANE(pane id injected via env at spawn) with the hook JSON on stdin. - Standard terminal notifications — OSC 9 (iTerm2/ConEmu), OSC 99 (Kitty), OSC 777 (rxvt/Ghostty/WezTerm) scanned from the PTY byte stream server-side. Any CLI/script can emit them; zero config.
- Foreground process info —
/proc: argv, cwd, child tree, CPU. Identifies which agent runs (codex,claude,opencode,cursor-agent) and whether it is forked/busy. - Terminal state — title (
OSC 0/1/2), BEL, cursor, alt-screen. Cheap, no parsing of natural language. - Output heuristics — last resort only (spinners, prompt shapes). Never the architecture's foundation; gated behind "no semantic signal for N seconds".
Adapter interface
| 1 | trait AgentAdapter: Send + Sync { |
| 2 | /// Does this adapter own the pane's foreground process? |
| 3 | fn identify(&self, proc: &ProcessInfo) -> bool; |
| 4 | /// Map a semantic event / poll snapshot to lifecycle + attention. |
| 5 | fn lifecycle_state(&self, ev: &AdapterEvent) -> LifecycleDecision; |
| 6 | /// Native session id, if the agent reports one. |
| 7 | fn session_identity(&self, ev: &AdapterEvent) -> Option<String>; |
| 8 | /// Map hook payload / OSC / exit to a notification (or none). |
| 9 | fn notification_event(&self, ev: &AdapterEvent) -> Option<NotificationDraft>; |
| 10 | /// Official resume argv for a persisted session id, if supported. |
| 11 | fn resume_capability(&self, session_id: &str) -> Option<ResumeCommand>; |
| 12 | /// Static metadata: kind, display name, hook install recipe. |
| 13 | fn metadata(&self) -> AdapterMetadata; |
| 14 | } |
Adapters: CodexAdapter, ClaudeCodeAdapter, OpenCodeAdapter,
CursorAdapter, GenericTerminalAdapter (process + title + BEL only).
Per-agent integration (verified Sept 2026, local CLIs)
Codex CLI (codex)
Resume:
codex resume [SESSION_ID],codex resume --last; non-interactivecodex exec resume <id|--last>;codex queue,codex fork,codex archive. Session ids are UUIDs/names.Hooks:
~/.codex/hooks.json/[hooks]inconfig.toml; events includeSessionStart,UserPromptSubmit,PreToolUse,PermissionRequest,PostToolUse,Stop,SessionEnd,SubagentStop. Payload on stdin carriessession_id,transcript_path,cwd,hook_event_name,turn_id. NoNotificationevent — usenotify = ["signaltty","hook-event",…](fires onagent-turn-complete) plustui.notifications→ BEL.Session env:
CODEX_THREAD_IDequals Stop payloadsession_id.Shim:
notifyentry + hook commands callingsignaltty hook-event --agent codex. Lifecycle:UserPromptSubmit→working,PermissionRequest→blocked+permission_required,Stop→done+unread.Verified against codex 0.157.1 (Sept 2026): hooks are trust-gated — new/changed entries sit at "review required" until the user picks "Trust all and continue" in an interactive session (hash recorded under
[hooks.state]in config.toml);codex execcannot grant trust, butcodex exec --dangerously-bypass-hook-trustfires hooks (SessionStart/UserPromptSubmit/Stop/SessionEnd all observed). Hook timeouts clamp to 3s — our installer uses 3. TheStophook REQUIRES stdout to be empty or JSON on exit 0 (plain text fails the hook), sosignaltty hook-eventprints human feedback to stderr and keeps stdout silent unless--json.Codex 0.159.1 can reuse a detached shared daemon that retains a previous terminal's
SIGNALTTY_PANE/ socket. Hooks run with the daemon environment, causinghook exited with code 1(NO_SUCH_PANE) or updating another pane. Direct managed local interactive launches, split and resume use native--no-daemonwhen supported (bounded version probe); original stored argv, config/auth/trust and hook commands remain unchanged. Unsupported clients or explicit remote sessions retain their command with an integration notice. Exec/utility commands are unchanged. See ADR-0014. When typing Codex in an ordinary shell, usecodex --no-daemon. To recover an existing session, exit the TUI and runcodex --no-daemon resume --lastin the same pane. Already-running clients keep their backend until restarted; Signaltty does not stop the shared daemon or bypass hook trust.
Claude Code (claude)
- Resume:
claude --resume [id],-c/--continue,--session-id <uuid>for pinned new sessions;--settings <file>for hook injection;-p/--printnon-interactive;claude attach/logs/stop/rm/agents. - Hooks: settings JSON (
~/.claude/settings.json, project/local,--settings); eventsSessionStart,UserPromptSubmit,PreToolUse,PostToolUse,Stop,SubagentStop,Notification(idle_prompt),PreCompact. Payloadsession_idequals$CLAUDE_CODE_SESSION_ID; survives-p --resume. - Shim: settings snippet installed by
signaltty integration install claude(user-scoped, non-destructive merge).Notification→ attention directly;Stop→ done+unread.
OpenCode (opencode)
- Resume:
opencode --session <id>/-s,--continue/-c; headlessopencode run --session <id>;opencode serve+ HTTP API;opencode attach,opencode session,opencode export. - Hooks: JS/TS plugin system (
~/.config/opencode/plugins, 25+ events):session.created/updated/idle/status,command.execute.before,event,config. Plugins receiveproperties.sessionIDand can shell out. - Shim: a small plugin file calling
signaltty hook-event --agent opencode.session.status busy/idle→ working/done; idle → done+unread.serveAPI is the future native-integration path.
Cursor Agent (cursor-agent)
- Resume:
--resume [chatId](picker when bare),--continue;--print --output-format json|stream-jsonheadless;--model,--mode plan|ask. - Hooks:
~/.cursor/hooks.json;sessionStartincludes a stablesession_idmappable to the CLI pid (must reject Desktop-origin events via process ancestry check). - Shim:
hooks.jsonentry + reporter script. Screen/title fallback aligned with Codex (session-only pattern, no output regex).
Session identity & resume (see also 09)
- Identity arrives via hook payload/env only — never scraped from
terminal text while a better channel exists. Exception: resumed
sessions may fire no
SessionStart(observed:muse resumeemits only prompt/Stop); the server therefore also acceptssignaltty report-session --pane <id> --session <sid>and keeps the last known id across respawns in the same pane. resume_capability()returns the official argv (table above). The server persists it but never auto-runs it: restore offers one-click/keypress resume per pane (LIVE/RESTORED/RESUMABLE/EXITED).
Claude permission-channel probe
A live probe on 2026-10-02 verified Claude PermissionRequest Once/Deny
verdicts, including two simultaneous signaltty panes. It also records terminal
answers, timeout fallback, and ignored late verdicts. The experiment ran against
eaf8076 with a response file standing in for the GUI choice. It does not
independently verify the subsequently implemented native response bridge.
See probe results and the
repeatable experiment.
Hook attribution (pane routing)
hook-event resolves its pane in two steps: explicit pane_id
(shims read $SIGNALTTY_PANE, injected by the server at spawn), else
process-ancestry fallback — the CLI sends its pid as client_pid and
the server walks /proc parents to the deepest live pane child. The
fallback survives sandboxed agents that strip hook env, and safely
ignores foreign hooks (Cursor Desktop shares hooks.json but its
processes are never our descendants → accepted, classified nothing).
Detection overlays: manifests (data, not code)
$XDG_CONFIG_HOME/signaltty/agents/*.toml (SIGNALTTY_AGENTS_DIR
override, --agents-dir on the server) extend one existing adapter kind
without recompiling — wrapper binaries, post-churn hook maps, custom
session keys. Loaded once at startup; malformed files are logged and
skipped, never fatal. signaltty integration status lists them.
| 1 | [agent] |
| 2 | kind = "codex" # required: an existing AgentKind |
| 3 | # display_name = "…" # optional override |
| 4 | # binaries = ["codex-wrap"] # merged with the builtin list |
| 5 | |
| 6 | [session] |
| 7 | # key = "thread_id" # payload session-key override |
| 8 | # resume = ["codex", "resume", "{session_id}"] |
| 9 | |
| 10 | [lifecycle.SomeFutureHook] # any hook; unmapped hooks fall through |
| 11 | # lifecycle = "working" |
| 12 | # attention = "unread" |
| 13 | # message = "…" |
Each field falls through to the builtin adapter independently when
absent. kind must stay inside the typed taxonomy — a genuinely new
agent still needs a Rust adapter. See ADR-0009.
Live process refresh (layer 4)
Spawn argv is only the first word. Every 10s the server re-reads each
live pane's deepest shell-transparent descendant (/proc: basename +
cwd): generic panes promote to the detected kind (never demote), and
pane.cwd follows /proc/<pid>/cwd. One pane.updated per changed
pane; silence otherwise (no event spam, no snapshot churn).
Fallback behavior
No hooks installed, unknown binary, or adapter disabled → pane works
as a plain terminal with GenericTerminalAdapter: title tracking,
BEL → unread, exit code → done/failed, OSC notifications still
honored. Zero-configuration agents (raw claude with no hooks) still
get OSC + title + exit semantics.
Automatic configuration (ADR-0013)
Starting a supported agent directly from the New Workspace dialog or pane.spawn,
pane.split, pane.resume prepares its hook integration before the process starts.
Preparation uses the actual executable basename, not agent_hint or detection
manifests. The ambiguous agent command is generic by default: it may be Grok;
known Cursor aliases can still be declared explicitly in detection manifests.
Only the selected provider is configured. Existing shells and agents already
running are not reconfigured; for agents typed inside a shell use
signaltty integration install <provider> first, or start a new workspace with
that agent selected. Restarting an agent may be needed after configuration.
Shared installation merges owned command hooks, preserves foreign hooks/keys,
refuses malformed files and unrelated OpenCode plugins, refreshes stale paths,
and uses bounded advisory locking and atomic replacement. Uninstall removes
only managed commands, including inside mixed groups. Claude CLAUDE_CONFIG_DIR,
Codex CODEX_HOME, OpenCode OPENCODE_CONFIG_DIR and the default XDG OpenCode
location are honored. Explicit CLI --home isolates provider home overrides;
OpenCode still honors XDG_CONFIG_HOME.
A setup failure keeps the terminal usable and returns integration.status=error
with a notice shown in the GUI/CLI. Explicit Claude --bare or setting sources
excluding user, and OpenCode --pure, return disabled without installing.
Installation never enables a user's disabled hooks. configured/CLI installed
mean files contain managed hooks, not that events have fired. For newly written
Codex hooks the notice asks to review/trust them in /hooks; Signaltty does not
alter [hooks.state], grant trust or inject trust-bypass flags.
Native permission replies (ADR-0015)
Managed Claude and Codex PermissionRequest hooks use a dedicated waiting
reporter. It converts the provider's session_id, tool_name and tool_input
into a structured prompt with Allow once and Deny. The GUI or CLI answer
returns through the live reporter as hookSpecificOutput.decision.behavior,
not as terminal keystrokes. Reporting-only requests have no native answer route
and render read-only.
The permission reporter has a 125-second provider timeout and a bounded 120-second server wait. No answer, cancellation, supersession, disconnect or server restart produces no permission verdict, so the provider retains its own permission flow. Answers validate the request's current pane, session and deadline. The transport is transient and cannot be restored from an audit log. Ordinary status reporters retain their short timeouts and silent stdout.
Current Codex 0.159.1 accepts the same narrow allow/deny output as Claude; its tagged hook engine honors configured timeouts without the earlier three-second limit. It rejects session permission updates, modified tool input and interrupt requests, so this integration offers no permanent Always policy. Hook trust remains Codex's own interactive review. The installer preserves foreign hooks and disabled settings as before.
Sources: Claude PermissionRequest reference, Codex 0.159.1 permission execution, Codex output parser.