# Coding Agent Hooks Architecture

Date: 2026-05-28

## Purpose

CTM runs several external coding agents as terminal sessions: Claude Code,
Codex, and Gemini CLI. Those agents own their terminal UI and provider runtime,
but CTM needs reliable lifecycle signals to render status, drive mobile status
lanes, preserve session context, and eventually trigger memory capture at the
right boundaries.

Hooks are the bridge. They are small user-approved entries in each agent's own
configuration that call back into CTM when an agent starts, stops, waits, or
uses tools. They complement, but do not replace, transcripts, PTY activity,
OpenTelemetry, and CTM's own session database.

## External Contracts

### Claude Code

Claude Code supports hooks in JSON settings files. Its docs describe a
read-only `/hooks` browser, user/project/local/plugin/session/built-in hook
sources, and command hooks that receive JSON on stdin. Common input fields
include `session_id`, `transcript_path`, `cwd`, and `permission_mode`.

CTM uses user-level `~/.claude/settings.json` so the integration applies to
Claude Code sessions launched through CTM without requiring project files to be
modified.

Source: https://code.claude.com/docs/en/hooks

### Codex

Codex has a different contract. CTM uses two surfaces:

- OTLP log export, configured through an `[otel]` block.
- The top-level `notify = [...]` command array, which Codex invokes on agent
  turn completion.

The Codex source defines `notify` as an optional command vector and shows it
invoked with an `agent-turn-complete` JSON payload. Current Codex config docs
also expose lifecycle-hook policy controls such as `allow_managed_hooks_only`
for managed environments.

Sources:

- https://github.com/openai/codex/blob/main/codex-rs/core/src/config/mod.rs
- https://github.com/openai/codex/blob/main/docs/config.md

### Gemini CLI

Gemini CLI uses JSON settings files, with user-level config at
`~/.gemini/settings.json` and project-level config at `.gemini/settings.json`.
Its hook settings include events such as `BeforeAgent`, `AfterAgent`,
`Notification`, `SessionStart`, `SessionEnd`, `BeforeTool`, and `AfterTool`.
The hook-writing guide emphasizes the same basic execution model CTM relies on:
hooks receive JSON on stdin, should log to stderr, and should reserve stdout for
JSON hook output.

Sources:

- https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/configuration.md
- https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/writing-hooks.md

### OpenTelemetry

OpenTelemetry OTLP defines HTTP endpoints such as `/v1/logs` for log export.
The OTLP exporter configuration supports environment variables including
`OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_PROTOCOL`; `http/json` is
a valid protocol value.

CTM uses this for low-latency agent status when an agent supports OTLP logs.

Sources:

- https://opentelemetry.io/docs/specs/otlp/
- https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/

## Current CTM Implementation

### UI

The AI Providers page has a **Coding Agents** tab. It checks:

- CLI availability through `/api/models/available-agents`.
- Hook install status through `/api/otel/status`.

The card state is derived in `public/index.html`:

- `Ready`: CLI detected and CTM hooks installed.
- `Enable live status`: CLI detected but CTM has not found the integration for
  the current CTM port.
- `Not found`: CLI is not on PATH.
- `Path changed` or `Config needs attention`: cached path or config inspection
  failed.

When coding agents are detected but hooks are missing, CTM shows one
**Enable live status** prompt for every detected runtime that needs the current
CTM port. The modal shows each target config path and a sanitized change
summary from `/api/otel/plan-install`, then applies the idempotent installer
only after the user approves. When hooks are already present, the card offers
**Refresh live status** for the same idempotent repair path.

The proactive prompt is allowed when the user opens the Coding Agents tab or
when a fresh coding-agent session emits `session.needsSetup`. Passive page load
still must not write config or install hooks.

Relevant code:

- `claude-task-manager/public/index.html`
  - `_modelsCodingAgentStatusFor`
  - `_ensureModelsAgentStatus`
  - `installCodingAgentHooks`
  - `renderModelsCodingAgentRoutes`

### Installer

`claude-task-manager/lib/agent-hooks-installer.js` is the local authority for
config writes. It has a plan/apply split:

- `planInstall(agent, port)` reads the user's config and builds the proposed
  next text.
- `install(agent, port)` applies the plan.
- `planUninstall(agent, port)` removes CTM-owned entries.
- `isInstalled(agent, port)` checks whether the current CTM integration marker
  is present.

The installer is deliberately idempotent. Re-running install should avoid
duplicates, and uninstall should remove only CTM-owned entries while preserving
user-owned config.

Current target files:

- Claude Code: `~/.claude/settings.json`
- Codex: `~/.codex/config.toml`
- Gemini CLI: `~/.gemini/settings.json`

Current hook commands point at CTM scripts under `claude-task-manager/bin`:

- `claude-hook.js`
- `codex-notify.js`
- `gemini-hook.js`

### Agent Presets

`claude-task-manager/lib/agent-presets.js` is the source of truth for each
agent's lifecycle events:

Claude Code:

- `SessionStart` -> `claude-hook start`
- `UserPromptSubmit` -> `claude-hook start`
- `Stop` -> `claude-hook stop`
- `Notification` -> `claude-hook idle`
- `PreToolUse` -> `claude-hook menu`

Codex:

- `notify` -> `codex-notify stop`
- OTLP env vars point logs at the CTM port.

Gemini CLI:

- `BeforeAgent` -> `gemini-hook start`
- `AfterAgent` -> `gemini-hook stop`
- `SessionEnd` -> `gemini-hook stop`
- `BeforeTool` -> `gemini-hook menu`

The same file also builds per-session environment variables, including
`CTM_SESSION_ID`, `CTM_PORT`, and, where supported, OTLP endpoint settings.

### Server Ingress

CTM only accepts hook/OTLP ingress from loopback. The local server handles:

- `/v1/logs` and `/api/otlp/v1/logs` -> `telemetryReceiver.handleLogs`
- `/hook/claude/{start|stop|idle|menu}` -> `telemetryReceiver.hookClaude`
- `/hook/codex/stop` -> `telemetryReceiver.hookCodexStop`
- `/hook/gemini/{start|stop|menu}` -> `telemetryReceiver.hookGemini`

The same receiver then broadcasts `session.status` over CTM's existing websocket
fan-out. This is important: hooks enter through a small local ingress, but they
reuse the normal session status contract instead of creating a parallel UI
state model.

Relevant code:

- `claude-task-manager/server.js`
  - hook/OTLP ingress near the local-only hook server block
  - `/api/otel/status`, `/api/otel/plan-install`, `/api/otel/install`,
    `/api/otel/uninstall`
  - session creation watchdog that prompts setup only when hooks are missing
    and the spawn is not a resume

### Tests

Existing tests cover the safety properties that matter most:

- `claude-task-manager/tests/agent-hooks-installer.test.js`
  - writes plausible Claude/Gemini settings
  - writes Codex `[otel]` plus top-level `notify = [...]`
  - install is idempotent
  - uninstall preserves user-owned config
  - legacy Codex `[notify]` section is repaired to the top-level array shape

- `claude-task-manager/tests/agent-presets.test.js`
  - agent detection
  - OTLP/env construction
  - inherited session identity sanitation

## End-to-End Flow

```text
User opens AI Providers -> Coding Agents
  |
  +-- UI GET /api/models/available-agents
  |     -> CTM discovers claude/codex/gemini binaries
  |
  +-- UI GET /api/otel/status
        -> CTM checks whether the relevant config contains CTM's current hook marker

User clicks Enable live status in the consent modal
  |
  +-- UI POST /api/otel/install?agent=<id>
        -> agent-hooks-installer reads user config
        -> patches only CTM-owned entries
        -> writes the agent config file

User launches an agent through CTM
  |
  +-- server builds env with CTM_SESSION_ID and CTM_PORT
  +-- for OTLP-capable agents, server injects OTLP endpoint env
  +-- agent runs normally in its terminal

Agent lifecycle event happens
  |
  +-- provider hook invokes CTM script
  +-- script posts JSON to localhost CTM hook endpoint
  +-- telemetry-receiver maps event to CTM session status
  +-- websocket broadcasts session.status
  +-- desktop/mobile UI updates lanes, badges, and attention state
```

## Architectural Benefits

### 1. Reliable Status Without Scraping Terminal Text

Terminal text is a weak status source. It changes with provider UI versions,
color themes, redraw timing, and terminal width. Hooks give CTM structured
signals at the lifecycle boundary: start, stop, idle, tool/menu, and agent
turn-complete.

CTM should still keep PTY and transcript fallbacks, but hooks reduce the amount
of brittle regex parsing needed for high-level state.

### 2. Provider-Neutral Session State

Claude, Codex, and Gemini expose different mechanics. CTM normalizes them into
one internal contract: `session.status`. This lets the web UI, mobile UI,
queue, notifications, and future Wall-E memory capture depend on one local CTM
event stream instead of three provider-specific contracts.

### 3. Low-Latency Mobile and Remote UX

The phone status page and desktop session list need to know when an agent is
actually running, waiting, or done. Hooks provide low-latency transitions that
do not require polling large transcripts or waiting for terminal text to settle.

This matters especially on mobile, where noisy lane movement and stale
`Running` badges are high-friction.

### 4. Correct Ownership Boundary

The provider still owns execution, tool calls, permissions, and terminal UI.
CTM only receives lifecycle facts and local telemetry. That keeps CTM from
pretending it is the agent runtime, while still giving CTM enough signals to be
a good control plane.

### 5. Future Memory Capture Boundary

Hooks are the natural point to trigger durable capture:

- `SessionStart`: link provider session ID to CTM session ID.
- `UserPromptSubmit` / `BeforeAgent`: mark a new work unit.
- `Stop` / `AfterAgent`: flush final transcript tail and summarize.
- `PreToolUse` / `BeforeTool`: detect approval/tool attention.
- Future compaction hooks: preserve memory before context disappears.

The existing `session-capture-foundation-design.md` already says hooks are
strong status signals but should not be the only source of truth. The better
architecture is "hooks as timing/evidence, transcripts as content, PTY as
fallback."

## Hooks-Optional Runtime Contract

Coding-agent hooks are not required for a CTM session to work. They improve the
status signal, but the baseline runtime path is still the PTY/session stream:

- CTM creates the PTY and renders terminal output regardless of hook status.
- `statusHooks` tracks meaningful PTY activity through the internal
  `SessionStateBus`, even when no project or global `.ctm-hooks.json` command is
  configured.
- `waiting-for-input` and `session-resumed` websocket messages continue to
  project running/waiting state through `SessionCapture` without a prior
  `session.status` hook frame.
- The missing-hook watchdog only emits `session.needsSetup` onboarding. It does
  not fabricate runtime status and does not block the session.

The intended degradation is therefore:

```text
With hooks:    provider hook/OTLP -> authoritative session.status -> UI
Without hooks: PTY/session stream -> state bus + terminal fallback -> UI
```

If either path disappears, that is a regression. Keep regression coverage for
both so hook improvements never turn into a hard runtime dependency.

## Consent and Safety Model

Installing hooks mutates persistent user-level files under the user's home
directory. That means the integration can affect future CLI sessions outside
the current CTM tab. It is useful, but it is also a trust boundary.

Therefore CTM should keep hook install user-approved. CTM can proactively
detect a missing integration and open the setup modal, but the persistent
config write still needs an explicit user action.

Required safety properties:

1. Preview before write for advanced users.
2. Idempotent install; no duplicate entries.
3. CTM-owned marker for uninstall/repair.
4. Preserve user-owned config and hooks.
5. Localhost-only ingress.
6. Clear "installed for this CTM port" semantics.
7. Visible uninstall/disable path.
8. No hook install during passive page load.
9. Do not expose raw config contents through preview APIs.

## Why CTM Should Not Auto-Install Hooks

There are three separate reasons.

First, it is a side effect outside the app workspace. The files are user-level
agent configs, not CTM-owned project files. Silent edits would violate the same
trust model that Claude/Codex/Gemini use for project trust and hook execution.

Second, hooks are executable persistence. A hook entry runs a command in future
agent sessions. CTM's entries are benign and local, but the class of action is
still security-sensitive. The UI should make the write explicit.

Third, the correct hook endpoint includes the CTM port. If a user runs multiple
CTM instances, auto-install on page load could race ports and rewrite a working
config for the wrong instance. User-initiated install gives the user control
over which CTM instance owns the integration.

## UX Recommendations

1. Use **Enable live status** for missing integrations and
   **Refresh live status** for installed integrations.
   - "Repair" implies breakage.
   - Both actions use an idempotent reinstall for the current CTM port.

2. Keep the setup preview in a consent modal.
   - Use existing `/api/otel/plan-install`.
   - Show target file path and a sanitized CTM-owned change summary.
   - Do not send or render raw current/next config text in the browser.

3. Add **Remove integration** when hooks are installed.
   - Use existing `/api/otel/uninstall`.
   - This makes the trust boundary reversible.

4. Explain the status in product terms:
   - CLI: "Can CTM launch this runtime?"
   - Hooks: "Can this runtime report live state back to CTM?"
   - Provider link: "Which model provider powers this runtime?"

5. Keep provider config separate.
   - Hooks do not configure API keys or default models.
   - Provider cards configure model access.
   - Coding Agent cards configure session runtime observability.

6. Prefer user-level config by default, but expose the target.
   - The current code writes user-level config.
   - If project-scoped hooks are added later, the UI must make scope explicit.

## Open Questions

1. Should CTM support project-scoped hook installs for teams that do not want
   user-level global hooks?
2. Should "Ready" require both hooks and a successful live test event, or is
   config presence enough?
3. Should Codex move from `notify`-only completion hooks to newer managed
   lifecycle hook config if/when the CLI exposes a stable user-level contract?
4. Should Wall-E memory capture become a first-class hook subscriber, or should
   it continue consuming normalized CTM session events?

## Proposed North Star

Hooks should be treated as a local, consented observability layer for coding
agents. The architecture should stay provider-neutral:

```text
Provider-specific hook/telemetry inputs
  -> CTM local ingress
  -> telemetry-receiver normalization
  -> session.status + capture events
  -> desktop/mobile UI, notifications, Wall-E memory, queues
```

This keeps the model clean:

- Coding agents own execution.
- AI Providers own model access.
- CTM owns orchestration, status, mobile/desktop control surfaces, and durable
  session evidence.
