> For the complete documentation index, see [llms.txt](https://faction-os.gitbook.io/faction-os-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://faction-os.gitbook.io/faction-os-docs/apps/hooks/readme_hooks.md).

# @factionos/hooks

The FactionOS hook handlers, provider hook maps, and persistent `ws-listener.js` bridge. The installed Claude compatibility map is what makes a real Claude Code session show up on your FactionOS battlefield today. The Codex map is the source template used by the CLI for user-level Codex hook install and `/hooks` trust-review workflow. The detailed hook-to-server contract is documented in [`../../docs/api/event-api-hook-contracts.md`](/faction-os-docs/docs/api/event-api-hook-contracts.md).

## What's in here

| File                              | Triggered by                                                                            | What it sends                                                                                                           |
| --------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `factionos-hero-spawn.js`         | `SessionStart`                                                                          | Spawns hero, starts `ws-listener.js` for the terminal                                                                   |
| `factionos-hero-active.js`        | `UserPromptSubmit`                                                                      | `hero_active` -> mission start through the saved session alias; Claude-compatible Notice Board prompt context when safe |
| `factionos-hero-idle.js`          | `Stop`                                                                                  | `hero_idle` -> mission complete through the saved session alias                                                         |
| `factionos-subagent-spawn.js`     | `PreToolUse[Task / Agent]`                                                              | `subagent_spawn` -> bounded opaque mission event                                                                        |
| `factionos-subagent-start.js`     | Codex `SubagentStart`                                                                   | `subagent_spawn` -> bounded opaque mission event from `agent_id` / `agent_type`                                         |
| `factionos-subagent-complete.js`  | `SubagentStop`                                                                          | `subagent_complete` -> bounded opaque mission event                                                                     |
| `factionos-awaiting-input.js`     | `PreToolUse[AskUserQuestion]`                                                           | `awaiting_input` -> first-class input wait event                                                                        |
| `factionos-input-received.js`     | `PostToolUse[AskUserQuestion]`                                                          | `input_received` -> first-class input received event                                                                    |
| `factionos-permission-request.js` | `PreToolUse[ExitPlanMode]` + `PermissionRequest`                                        | `permission_request` -> first-class permission request event, without blocking approval state yet                       |
| `factionos-git-guard.js`          | `PreToolUse[Bash]` (first in chain)                                                     | `git_dirty` warning with compact Git attribution metadata; blocks destructive commands in restricted mode               |
| `factionos-bash-command.js`       | `PreToolUse[Bash]` (second in chain)                                                    | `bash_command` -> generic tool use plus first-class bash event                                                          |
| `factionos-file-access.js`        | `PreToolUse[Read / Write / Edit]`; Codex `apply_patch` and selected MCP file-like tools | `file_access` -> generic tool use plus first-class file event with repo-relative attribution metadata when safe         |
| `factionos-tool-result.js`        | Codex `PostToolUse[Bash / apply_patch / MCP]`                                           | `tool_result` -> compact status, response type, and count metadata only                                                 |

Plus:

* `ws-listener.js` - one long-lived Node process per terminal. Maintains a persistent WebSocket to the FactionOS server so subsequent hook fires don't pay the cost of reconnecting. Drains a per-host spool directory when the server has been briefly down.
* `_lib.js` - shared helpers (stdin reader, bounded POST, TTY detection, CLI detection, atomic local writes, sanitized spool fallback, structured logging).
* `hooks.json` - the compatibility alias that Claude Code reads when this is installed into `~/.claude/settings.json`.
* `hooks.claude.json` - the provider-specific Claude source map. It mirrors `hooks.json` byte-for-byte for the current release.
* `hooks.codex.json` - the provider-specific Codex source template installed by `factionos init --cli codex` into user-level Codex hook state.
* The current event ingest contract is summarized in [`../../docs/api/README_api.md`](/faction-os-docs/docs/api/readme_api.md) and detailed in [`../../docs/api/event-api-hook-contracts.md`](/faction-os-docs/docs/api/event-api-hook-contracts.md).

Provider map source notes:

* `hooks.json` is the Claude Code compatibility map installed into `~/.claude/settings.json`.
* `hooks.claude.json` mirrors `hooks.json` and is the provider-specific Claude source map.
* `hooks.codex.json` is a source template, not a project-local trusted `.codex/` layer. The CLI reads it, injects the installed package root and managed environment values, then merges it into user-level `hooks.json`.
* FactionOS-managed Codex command hooks are identifiable by the installed hook script path plus `FACTIONOS_CLI=codex-cli`. Reinstall removes older managed commands before writing the current map and leaves user-owned hooks in place.

Current server note: hook handlers send `eventName` plus a hook-specific `type`. The server dispatches on `eventName` or `hook`, resolves later payloads through the saved `sessionId` alias, and now maps `file_access`, `bash_command`, `awaiting_input`, `input_received`, and `permission_request` to first-class protocol events where enough data is present. Subagent spawn and completion payloads now produce typed lineage and mission graph updates when safe identifiers are available; malformed or missing identifiers degrade to bounded lineage or mission-event summaries. File-access payloads may include safe `repoRelativePath`, `repoRelativePaths`, and `attributionStatus` fields so the server can promote them into command-center file intents. Git guard payloads may include `gitOperation`, `gitRisk`, and `attributionStatus` so the server can record compact Git diagnostics. Hooks still do not execute repository changes.

## Provider-neutral normalization boundary

The hook runtime now normalizes common provider field differences before handlers build FactionOS payloads. Shared helpers accept Claude-shaped and Codex-shaped keys for hook event names, session ids, model ids, permission modes, prompt text, tool names, tool input, tool response, and subagent metadata. Malformed external input degrades to empty objects, missing-field statuses, or compact degraded reasons instead of throwing.

This is a runtime and source-map compatibility layer. The CLI owns installation into local agent config files. `hooks.json` remains the Claude Code compatibility map, `hooks.claude.json` mirrors it, and `hooks.codex.json` is the Codex source template consumed by `factionos init --cli codex`. This package does not generate project-local `.codex` files, create trusted hook state, add custom slash commands, bypass `/hooks` trust review, package a Codex plugin, or claim production-hosted telemetry. Phase 11 validation evidence is complete. Phase 12 Session 01 explicitly defers plugin packaging because direct user-level hooks are the validated supported path for this release.

Diagnostics and spool fallback entries summarize Codex-sensitive provider fields before writing local files. Raw prompts, nested tool input, tool responses, transcript paths, agent transcript paths, MCP argument bodies, token-like values, command output, broad local paths, patch bodies, and diff bodies are redacted or replaced with compact counts and keys.

## Codex hook map readiness

Phase 09 Session 03 completed source-controlled Codex hook-map assets and handler coverage for Codex-shaped hook payloads. Phase 10 Session 02 wires that map into the CLI's user-level Codex installer. `factionos init --cli codex` reads this template, backs up the first raw user `hooks.json` when present, merges FactionOS-managed command hooks, and leaves user-owned hooks in place. It is still not project-local `.codex` generation, trusted hook state, production validation, custom slash command support, plugin packaging, or a claim that FactionOS can bypass Codex hook trust.

Source-backed Codex constraints reviewed on 2026-05-31:

* Codex loads hooks from `hooks.json` or inline `[hooks]` config next to active config layers, including user-level `~/.codex` and trusted project-local `.codex` layers. See <https://developers.openai.com/codex/hooks>.
* Codex requires non-managed command hooks to be reviewed and trusted before they run. Users review loaded hooks with `/hooks`; changed hook definitions are skipped until trusted again.
* Codex can load hooks from enabled plugins through a plugin manifest or default `hooks/hooks.json`, but plugin-bundled hooks use the same non-managed trust-review flow. Installing or enabling a plugin does not automatically trust those hooks. See <https://developers.openai.com/codex/plugins/build>.
* Matching hooks from multiple sources all run, and multiple matching command hooks for the same event run concurrently. FactionOS handlers must not rely on ordering to prevent another matching hook from starting.
* Codex matcher support is event-specific: `PreToolUse`, `PermissionRequest`, and `PostToolUse` match tool names; `SessionStart` matches start source; `SubagentStart` and `SubagentStop` match agent type; `UserPromptSubmit` and `Stop` ignore matcher values.
* `apply_patch` hook matching uses the canonical `apply_patch` tool name and also accepts `Edit` or `Write` matcher aliases. Hook input still reports `tool_name: "apply_patch"`.
* `PermissionRequest` can allow, deny, or decline to decide, but FactionOS remains observation-only in this package and does not return approval decisions.
* `PostToolUse` runs after supported Bash, `apply_patch`, and MCP tools have already produced output. It cannot undo side effects.
* Project-scoped `.codex/config.toml` layers load only for trusted projects and cannot override provider, auth, notification, profile, or telemetry routing keys. See <https://developers.openai.com/codex/config-reference#configtoml>.
* Codex ships built-in slash commands such as `/hooks`, `/permissions`, `/status`, and `/mcp`. This package does not add custom Codex slash commands. See <https://developers.openai.com/codex/cli/slash-commands#built-in-slash-commands>.

Plugin reuse boundary:

* A future plugin package must reuse this hook map, these handlers, and the shared redaction helpers. It must not fork a second runtime path or a looser privacy contract.
* Phase 12 Session 01 does not create `.codex-plugin/`, a repo marketplace entry, plugin cache state, or plugin-bundled hooks.

## Installing

The hook scripts are designed to be installed by the FactionOS CLI:

```bash
npm install -g @factionos/cli
factionos init
```

This defaults to the Claude provider route and merges `hooks.json` into `~/.claude/settings.json` (with a `settings.backup.json` created first), then sets `CLAUDE_PLUGIN_ROOT` to point at this package's install location.

The explicit equivalent is:

```bash
factionos init --cli claude
```

`factionos init --cli codex` and `factionos init --cli all` install the Codex source template into user-level `hooks.json`, respecting `CODEX_HOME` when it is set. The CLI backs up the first raw hooks file to `hooks.backup.factionos.json`, removes older FactionOS-managed command hooks, preserves user-owned hooks, and writes the merged file atomically. It does not write Codex auth, provider, telemetry, model, profile, API key, account settings, trusted-project state, or hook trust decisions.

`factionos uninstall --cli codex` restores `hooks.backup.factionos.json` when that first-run backup exists. Without a backup, it removes only FactionOS-managed Codex command hooks and leaves user-owned hooks in place. If `hooks.json` is malformed and no backup is available, uninstall reports the condition and leaves the file untouched.

The install and uninstall scripts are idempotent and settings-safe:

* The first raw `~/.claude/settings.json` is backed up to `~/.claude/settings.backup.json` before managed changes are written.
* Malformed existing settings are preserved in the backup and replaced with a fresh settings object instead of crashing install.
* Re-running install replaces only FactionOS-managed hook commands and keeps unrelated user hook entries, including entries on the same Claude hook event.
* Uninstall restores the backup when it exists. Without a backup, it surgically removes only FactionOS-managed hook commands and managed FactionOS env values.
* Settings writes use temp-file-plus-rename so partial writes are not left as `settings.json`.

Codex trust and minimization checklist:

* After `factionos init --cli codex`, users review loaded hooks with `/hooks` in a new Codex CLI session.
* Managed Codex hook entries are observation-only and timeout-bound. They post compact local events to the FactionOS server and do not return approval decisions for Codex.
* User-owned Codex hooks stay in place. FactionOS replaces only older FactionOS-managed command hooks before writing the current managed map.
* Prompt bodies, transcript contents, MCP argument bodies, raw patch bodies, command output, token-like values, and broad local paths are blocked from normal diagnostics and docs examples.

Hook handlers and the listener auto-load env files in this order when run from this monorepo: root `.env.local`, root `.env`, `apps/hooks/.env.local`, then `apps/hooks/.env`. Already-exported shell variables keep priority over file values, earlier files win over later files, and blank placeholders are ignored. Prefer root `.env.local` for normal local deployment.

## Design contract (must hold)

1. **Hook handlers must NEVER block indefinitely.** Every POST and prompt-start Notice Board context GET is bounded by a 1.5 s timeout. If the server is down, event POSTs write a sanitized local spool entry and still exit successfully; context GET failures do not spool.
2. **Hook handlers must not write observation noise to stdout/stderr.** The only normal stdout exception is `factionos-hero-active.js` on Claude-compatible `UserPromptSubmit`, where it may emit provider contract JSON with `hookSpecificOutput.additionalContext`. Codex and unknown providers remain silent and use the CLI Notice Board commands instead.
3. **All handlers exit 0 unless explicitly denying a tool call.** The only handler that may exit non-zero is `factionos-git-guard.js` when `FACTIONOS_RESTRICTED=true`.
4. **State files in `~/.factionos/` use atomic writes.** Write to `*.tmp` then rename - never partial.

## Notice Board prompt context

On `UserPromptSubmit`, `factionos-hero-active.js` resolves the current session id and attempts a bounded `GET /notice-board/context?sessionId=<sessionId>` against `FACTIONOS_SERVER_URL`. The GET includes `Authorization: Bearer <token>` only when `FACTIONOS_AUTH_TOKEN` is configured. Missing session ids, unreachable servers, timeouts, non-2xx responses, malformed JSON, unsupported response shapes, oversized context, and unsafe payload markers all degrade to no context without stderr or spool entries.

For Claude-compatible providers, a valid context response is written as:

```json
{"hookSpecificOutput":{"additionalContext":"..."}}
```

The additional context contains only the server `context` string plus the fixed reminder to post brief updates to the Notice Board. A valid empty `{ "context": "" }` response emits the reminder only. Codex, Cursor, OpenCode, OpenClaw, unknown providers, and failed lookups remain silent; agents can use `factionos notice` or the Notice Board command path added by the CLI package.

The hook output boundary accepts only a bounded string `context` field. It ignores route metadata and rejects known unsafe side-channel fields or text markers for raw prompts, transcripts, terminal output, command output, file contents, secrets, broad local paths, logs, exports, replay buffers, scans, media drafts, diagnostics, backups, and quarantined content. The formatter also redacts token-like values and broad local paths as a final guard before stdout.

| Context lookup outcome                                                                                       | Claude-compatible providers                                                                                         | Codex, Cursor, OpenCode, OpenClaw, unknown providers           |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Safe non-empty context                                                                                       | Emit bounded `additionalContext` plus the fixed reminder.                                                           | Stay silent; use `factionos notice` for command-access parity. |
| Safe empty context                                                                                           | Emit the fixed reminder only.                                                                                       | Stay silent.                                                   |
| Missing session id, timeout, network failure, non-2xx, malformed JSON, non-string context, oversized context | Treat as no context without stderr or spool entries; event POST behavior continues independently.                   | Same no-context fallback and no stdout/stderr.                 |
| Blocked fields or unsafe context markers                                                                     | Reject the context, log only compact status labels, and emit nothing except the safe reminder path when applicable. | Reject the context and stay silent.                            |

## Runtime diagnostics and fallback

* Normal handler execution is silent. `stdout` and `stderr` stay empty unless `factionos-git-guard.js` intentionally denies a restricted command or `factionos-hero-active.js` emits Claude-compatible Notice Board additional context.
* Handler diagnostics go to `~/.factionos/hooks.log` as JSON lines with handler names, event kinds, timestamps, status, durations, sizes, and safe state identifiers. Diagnostics do not include raw payloads, full prompts, command bodies, terminal output, secret values, or file contents.
* Phase 03 orchestration diagnostics add safe event-family summaries, POST status, bounded durations, counts, and sanitized session, terminal, subagent, request, and tool-use identifiers. Raw queue entries, proposal rationale, transcript paths, spool payloads, and malformed input bodies are not logged.
* When `/event` cannot be reached within the bounded timeout, handlers write a sanitized JSON file under `~/.factionos/spool/`. Spool entries are bounded in size and count and may contain truncated event previews needed for local recovery.
* The listener records spool pump counts, malformed-frame byte counts, reconnect attempts, and server-message type labels. It deletes malformed spool files during normal pumping, preserves valid entries when the WebSocket is closed, and never logs raw malformed spool contents.
* CLI recovery can remove stale listener PID files and malformed spool JSON entries when the operator explicitly runs `factionos doctor --recover-orchestration`. That recovery does not remove valid spool entries, settings, lifecycle files, session archives, memory, browser state, or workspace files.
* Missing or malformed session id files fall back to explicit missing-session behavior instead of crashing a hook process.

## Environment variables

| Variable                                    | Default                 | Effect                                                                              |
| ------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------- |
| `FACTIONOS_SERVER_URL`                      | `http://localhost:2468` | Where to POST events                                                                |
| `FACTIONOS_AUTH_TOKEN`                      | unset                   | Optional bearer token when the local server has token auth enabled                  |
| `FACTIONOS_HOME`                            | `~/.factionos`          | State + log directory                                                               |
| `FACTIONOS_FACTION`                         | unset                   | When set, new heroes spawn into this faction. Otherwise the server picks one.       |
| `FACTIONOS_RESTRICTED`                      | unset                   | When `true`, git-guard blocks destructive commands instead of just observing        |
| `FACTIONOS_CLI`                             | auto-detect             | Override the detected CLI identifier                                                |
| `FACTIONOS_HOOK_TIMEOUT_MS`                 | `1500`                  | Override the bounded POST and Notice Board context GET timeout for hook handlers    |
| `FACTIONOS_HOOK_JSON_MAX_BYTES`             | `65536`                 | Maximum response bytes accepted by bounded hook JSON GET helpers                    |
| `FACTIONOS_NOTICE_CONTEXT_SOURCE_MAX_CHARS` | `6000`                  | Maximum server context characters accepted before the hook treats context as absent |
| `FACTIONOS_NOTICE_CONTEXT_OUTPUT_MAX_CHARS` | `4000`                  | Maximum provider additional-context characters emitted by hero-active               |
| `FACTIONOS_MAX_SPOOL_FILES`                 | `100`                   | Maximum local spool files retained while the server is down                         |
| `FACTIONOS_MAX_SPOOL_BYTES`                 | `8192`                  | Maximum bytes per local spool entry                                                 |
| `FACTIONOS_DISABLE_LISTENER`                | unset                   | Test-only escape hatch to skip listener spawn from `SessionStart`                   |

## Debugging

```bash
# Watch hook activity in real time
tail -f ~/.factionos/hooks.log

# Watch listener activity for a specific terminal
tail -f ~/.factionos/listener-<tty>.log

# Manually fire an event (useful for testing)
echo '{"session_id":"test","user_message":"hello"}' | \
  node ./src/factionos-hero-active.js
```

## Why per-terminal listener (vs. per-hook POSTs only)?

A hook handler can't open a WebSocket and exit quickly enough to keep Claude responsive. POST-per-hook works but adds 5-20 ms to every tool call. The `ws-listener.js` pattern gets the best of both: hooks POST cheaply, the listener provides a persistent bidirectional channel so the server can push directives back (permission responses, faction reassignments, restart pings) without polling.

Current listener limits:

* The listener is optional. Hook handlers do not require it to be running.
* One listener is started per sanitized terminal id when possible; stale PID files are removed before a new listener starts.
* Reconnect backoff is capped at 30 seconds.
* Spool draining handles up to 20 files per pump and does not retry malformed entries.
* The listener does not execute inbound chat commands or spawn Claude from remote WebSocket frames.
* Listener diagnostics report statuses, counts, durations, bytes, and sanitized identifiers only.
* Phase 01 tests cover hook payload shaping, listener lifecycle behavior, installer integration, quiet failure paths, and selected `/event` ingest mappings. Broader orchestration commands remain future work, not current hook behavior.

## Phase 03 Orchestration Boundary

Phase 03 promotes selected hook metadata into typed local lineage and mission graph updates while keeping handlers silent and timeout-bound. Phase 19 Session 08 additionally lets file-access and git-guard payloads carry compact repo-relative attribution metadata for command-center file intents and Git diagnostics. These hook payloads remain observations; executor decisions and Git workbench execution live on the local server behind the guarded command-center boundary. Hook and listener diagnostics summarize event families, counts, durations, status labels, and sanitized identifiers only.

Hooks do not accept inbound chat commands, spawn remote agents, execute queued tasks, run terminal commands from server frames, or perform unified erasure. Diagnostics must not log raw prompts, command bodies, terminal output, file contents, tokens, transcript paths, raw queue entries, proposal rationale, or raw spool payloads.

## Phase 06 Hook And Executor Boundary

Phase 06 Session 01 keeps hooks as local observability and diagnostics producers only. Future isolation diagnostics may add compact executor posture signals, but hooks must not run file, git, terminal, Docker, remote, or hosted commands from server, Worker, chat, webhook, or browser frames unless a later approved executor threat model adds consent, authorization, audit, rollback, redaction, tests, and docs.

Hook and listener diagnostics must continue to summarize event families, statuses, durations, counts, sizes, and sanitized identifiers only. They must not leak prompts, command bodies, terminal output, file contents, transcripts, tokens, broad paths, diagnostics payloads, logs, backups, or raw spool data.

Session 05 keeps hook-side isolation behavior non-executing. Hook handlers and the listener may report compact executor posture or diagnostic family labels in future payloads, but they must not accept inbound execution instructions, run file or git operations, run terminal commands, start containers, call remote runners, execute Worker commands, or treat server/Worker/browser frames as authority to mutate local state. Timeout-bound silent failure and sanitized local logging remain the required behavior.

## Phase 06 Closeout

Phase 06 closes the hook-owned diagnostics and executor boundary. Hook handlers remain sanitized local observability producers and the listener remains a bounded local bridge. They do not run queued tasks, accept inbound chat or Worker commands, mutate files, execute git or terminal operations, start containers, call remote runners, perform hosted diagnostics, or erase local or Worker state.

Hook and listener output must stay quiet or compact: event families, status labels, counts, durations, byte counts, and sanitized identifiers only. Raw payloads, prompts, command bodies, terminal output, file contents, transcripts, tokens, broad paths, diagnostics payloads, logs, backups, and raw spool data remain blocked from normal output and diagnostics.

## Phase 07 Hosted Transfer Boundary

Phase 07 Session 01 does not add hosted hook transfer. Hook handlers and the listener remain local observability producers that POST to the configured local server and spool sanitized local entries on bounded failures.

Future hosted diagnostics, remote access, push, analytics, public replay, hosted storage, Worker command, or executor behavior must not use hook payloads until a later approved session defines consent, authorization, redaction, timeout, retry, local fallback, audit, tests, and docs. Hooks must continue to block raw prompts, command bodies, terminal output, file contents, transcripts, tokens, broad paths, diagnostics payloads, logs, backups, and raw spool data from hosted payloads by default.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://faction-os.gitbook.io/faction-os-docs/apps/hooks/readme_hooks.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
