Architecture

How hook commands flow from the plugin through the SubQ CLI to the local server and back.

Request Flow

Five steps from hook event to provider response. The plugin declares, the CLI wraps, the server handles.

RPC Flow
1. hook.fire Claude Code fires lifecycle event TRIGGER
2. hooks.json Plugin routes to shell command via hooks.json ROUTE
3. cli.forward CLI reads stdin, wraps in hook envelope, forwards to local server WRAP
4. server.handle Server delegates to packages/hooks for normalization + handling HANDLE
5. cli.translate CLI translates server decision to provider-native stdout RESPOND
Fallback: If the local server is unreachable, blocking hooks (PreCompact) timeout and pass through. Non-blocking hooks (Stop, StopFailure) return {}.

Hook Envelope

The CLI wraps provider-native stdin payloads in a stable envelope before forwarding to the server. Seven fields, versioned schema.

schemaVersion
Integer version of the envelope format. Currently 1. Allows the server to handle envelope evolution without breaking older CLIs.
provider
String identifying which AI coding agent fired the hook: "claude", "codex", or "gemini".
event
The lifecycle event name in snake_case: session_start, stop, pre_compact, stop_failure.
providerRef
Opaque reference string from the provider (e.g., session ID). Enables correlation across hook events within a session.
projectPath
Absolute path to the working directory. Used by the server to look up project-specific state and observations.
receivedAt
ISO-8601 timestamp when the CLI received the event. Server uses this for cooldown logic (e.g., Stop's 3-minute gate).
payload
The raw provider-native stdin payload, passed through unmodified. Shape varies by provider and event type.
Example Envelope
{
  "schemaVersion": 1,
  "provider": "claude",
  "event": "session_start",
  "providerRef": "abc",
  "projectPath": "/repo",
  "receivedAt": "2026-04-22T00:00:00.000Z",
  "payload": {}
}

Hook Decisions

The server returns one of two decisions. The CLI translates the decision into the provider's native stdout format.

NON-BLOCKING

Observe

Record the event silently. No output to the provider. Used by Stop (handoff saved server-side) and StopFailure when no advisory is needed.

Server returns: { "decision": "observe" }

CLI emits: {}

BLOCKING

Replace

Inject content into the provider's context. Used by SessionStart (structural overview) and PreCompact (structured handoff before lossy compaction).

Server returns: { "decision": "replace", "content": "..." }

CLI emits: { "hookSpecificOutput": { "additionalContext": "..." } }

Package Boundaries

Four concentric layers from the plugin declaration shell to the monorepo core. Each layer owns a single responsibility.

Plugin (this repo)

Declares hooks.json and hosts generated skills/*/SKILL.md. Zero executable code. Installed via claude plugin add.

apps/cli + apps/server

CLI: Thin forwarder—envelope wrapping, provider translation, stdin/stdout. Server: RPC surface, hook handling coordination, observation recording.

packages/hooks + packages/contracts

hooks: Provider-specific normalization and handler logic. contracts: Hook envelope types, decision types, provider identity.

packages/store + packages/providers

store: SQLite repositories for observations. providers: Provider-specific content parsers (transcripts, sessions).