Skip to content
Apothem
Reference

Hooks

Tool event handlers that fire on SessionStart, PreToolUse, PostToolUse, and Stop, loading message contexts enforcing git isolation and plan discipline, with a tested runtime contract and per-user state.

Hooks fire on tool events and load message contexts that the assistant treats as additional system prompts. Wiring lives in the Claude Code settings.json hook block (engine install) or the plugin's hooks.json (plugin install), the Codex hooks.json hook file, the Qwen Code settings.json hooks map, the CodeBuddy .codebuddy/settings.local.json hooks key (SessionStart and PostToolUse only), the Kiro hook file .kiro/hooks/apothem.json (SessionStart only), the GitHub Copilot hook file .github/hooks/apothem.json (sessionStart and postToolUse only), the Windsurf (Devin Local) .devin/config.local.json hooks key (SessionStart and PostToolUse only), and the Cursor project hook file .cursor/hooks.json (sessionStart only).

EventTimeoutPurpose
SessionStart30 secondsRead memory, check plan state, report readiness; after compaction, deliver the recovery context; flag a knowledge base that changed since its baseline
PreCompact30 secondsVerify state externalized before compaction (Codex, Qwen Code; not registered on Claude Code)
PostCompact30 secondsRestore working state after compaction (Codex, Qwen Code; not registered on Claude Code)
Stop60 secondsSession-end protocol, opt-in
PostToolUse10 secondsContext-size advisory: save state at the first mark, and at the second the operator is told the session is long; at most twice per compaction cycle; runs in the background on Claude Code
PreToolUse (shell: Bash, PowerShell, run_shell_command)10 secondsGit-artifact isolation, plans locality, dynamic-eval guard
PreToolUse (Write/Edit/NotebookEdit/apply_patch)10 secondsPlan-reference isolation, authorship header, plans locality, dependency and dynamic-eval guards
PreToolUse (AskUserQuestion)10 seconds(Recommended)-marker well-formedness

Dispatcher

Engine-installed hook entries invoke the dispatcher by absolute script path with an install-resolved interpreter. The templates carry the ${PYTHON_BIN} and ${HARNESS_ROOT} tokens, which the installer renders at install time. Claude Code and Qwen Code materialize the dispatcher under <harness-root>/.apothem/support/hooks/; Codex under <harness-root>/hooks/. Claude Code entries use exec form (command plus args), so they need no shell. The Claude Code plugin runs each hook as bash "${CLAUDE_PLUGIN_ROOT}/lib/apothem/hooks/lib/bootstrap.sh" <event> [<message-file> ...], in Claude Code's default hook shell (sh on macOS and Linux, Git Bash on Windows); the bootstrap locates a Python 3.10+ interpreter and runs the bundled dispatcher. It caches the interpreter it found in interpreter-path (interpreter-path-ps1 for bootstrap.ps1) under the hook state directory (below) and reuses it while the interpreter still exists, so the PATH probe runs once rather than before every tool call; delete the cache file to force a new probe. Only an absolute state directory and an absolute cached path are honoured.

Every surface registers one dispatcher entry per event and matcher, listing every message that matcher evaluates: one process reads the payload once, runs each guard, and answers with one envelope, so a Write starts one dispatcher rather than one per guard. The Claude Code engine install also runs the conformity gate (gate.py --hook) on Write and Edit as its own entry. On Windows without Git for Windows, use the engine install (apothem install --harness claude-code), whose exec-form hooks need no shell. The dispatcher is a standalone stdlib script: it bootstraps its own sys.path, so no importable apothem package is needed on the host. From a source checkout, the module form python -m apothem.hooks.dispatch <event> (with PYTHONPATH=src) runs the same code for development. The dispatcher loads hooks/lib/resolve_root.py for ecosystem-root resolution and delegates to hooks/emit_hook_context.py and hooks/session_start_bootstrap.py.

Vendor contracts

A harness that is not Claude Code runs the same dispatcher with --contract <id> after the script, for example "<python>" "<project>/.apothem/support/hooks/dispatch.py" --contract codebuddy SessionStart. The contract (hooks/lib/vendor_contract.py) translates the vendor's event and tool-name spellings and payload keys into the Claude shape the handlers read, rewrites the envelope into the shape the vendor reads, and drops an event the vendor does not hand to the model. Without the flag nothing is translated and the output is the Claude envelope, byte for byte. The flag is part of the documented command line of the dispatcher: dispatch.py [--contract <id>] <Event> [<message> ...].

ContractReadsOutputDelivered events
codebuddyClaude's stdinthe Claude envelopeSessionStart, UserPromptSubmit, PostToolUse
devinClaude's stdin with lowercase tool names (exec, write, edit); the project root from DEVIN_PROJECT_DIRthe Claude envelopeSessionStart, UserPromptSubmit, PostToolUse
cursorcamelCase events, conversation_id, workspace_roots{"additional_context": ...}SessionStart
copilotcamelCase sessionId, toolName, toolArgs, or the PascalCase snake_case form{"additionalContext": ...}SessionStart, PostToolUse, Notification
kiroClaude-like stdin, Kiro tool ids (execute_bash, fs_write, str_replace)bare textSessionStart, UserPromptSubmit
claudeClaude's stdinthe Claude envelopeevery event

The codebuddy, copilot, cursor, devin and kiro contracts are installed by an Apothem adapter today (devin by the Windsurf adapter); the claude contract proves the default command line is unchanged. The dispatcher stays advisory under every contract: no shape carries a permission, a decision, a block or a rewritten tool input.

Cursor and Devin Local load the hooks Claude Code's settings files hold as well as their own, so with the Apothem Claude Code install on the same machine two sets can run. The SessionStart bootstrap is delivered once: the vendor's own command (--contract cursor or --contract devin) and the Claude Code copy, which is the default command line, share one marker per session and source in the hook state directory, and whichever runs second is silent. The default command line takes part only when the process environment names the host (CURSOR_PROJECT_DIR, CURSOR_VERSION or DEVIN_PROJECT_DIR); under Claude Code, Codex and Qwen Code it never reads the marker. The marker binds for ten seconds, so a later start of the same session, such as a second compaction, still delivers its recovery context. No PostToolUse counter is de-duplicated, so the context-size counter runs in both places and reaches its thresholds early. The install prints a note that names the vendor's switch for turning the import off (Cursor's "Include Third-Party Plugins, Skills, and Other Configs"; Devin's read_config_from.claude).

A command registers only where the vendor hands its output to the model. Kiro adds a hook's stdout to the context for SessionStart and UserPromptSubmit only, so the Kiro file registers SessionStart alone. Copilot reads additionalContext from sessionStart and postToolUse and only permission fields from preToolUse, so the Copilot file registers those two events, under their camelCase names, and no guard. Each Copilot entry carries a bash and a powershell command. Both files are Apothem-owned and name the interpreter and project path of the machine that ran the install; the harness pages say what that means for a file a repository commits.

Devin Local reads additionalContext from SessionStart, UserPromptSubmit and PostToolUse and documents none for PreToolUse, so the Windsurf registration holds SessionStart and PostToolUse, with no matcher (Devin's matcher is a regular expression, and an omitted one matches every tool). It goes in the local override .devin/config.local.json, which Devin documents as gitignored and excludes from git itself through .git/info/exclude. Cursor's sessionStart hook adds additional_context to the conversation's initial context; the first version registers that event alone. Cursor's .cursor/hooks.json is the one project hook file, so it is the operator's: the install merges one entry into it, keeps the operator's entries and keys, refuses a file it cannot rewrite without loss, and tells its own entry apart by the dispatcher path in the command. Cursor's desktop app runs hook commands through PowerShell on Windows, so on a Windows host the command starts with & .

Across all five files an entry is Apothem's when its command runs .apothem/support/[<harness>/]hooks/dispatch.py with --contract <id>, whichever project path it was written under. A renamed, moved or cloned project therefore keeps one set of entries, and a hook of yours, such as a hooks/dispatch.py in your own tree or python -m apothem.conformity.gate, is never replaced or removed. apothem verify reports the registration missing once none of Apothem's entries is left in the file, and an uninstall that cannot read the file reports it as skipped. A project or interpreter path that holds $, a backtick, a double quote or a backslash is refused at install, naming the path, because the quoted command would run a different file.

Message contexts

Per-event Markdown contexts live under src/apothem/hooks/messages/:

  • Session lifecycle: sessionstart.md, skill-index.md, precompact.md, postcompact.md, stop.md, posttooluse-proactive-compaction.md
  • Shell calls: pretooluse-bash.md, pretooluse-bash-plan-guard.md
  • File writes: pretooluse-write.md, pretooluse-edit.md, pretooluse-notebookedit.md, pretooluse-write-plan-guard.md, pretooluse-write-header-guard.md, pretooluse-edit-header-guard.md, pretooluse-conformity.md
  • Guards on content: pretooluse-dependency-guard.md, pretooluse-eval-guard.md
  • Questions: pretooluse-askuserquestion-recommended.md

On Claude Code, postcompact.md is delivered by the SessionStart hook when the session resumes after compaction, because Claude Code does not pass PreCompact or PostCompact hook output to the assistant.

skill-index.md is generated, not written: scripts/dev/generate_skill_index.py renders it from the skill and command catalog, one line per task group, with manual skills shown as /apothem:<name>. On Claude Code the SessionStart hook adds it to the session context when the file exists. It exists because Claude Code drops skill descriptions from its listing once the listing outgrows its character budget, and the index keeps every skill findable by task. A plugin install shows manual skills as /apothem:<name>; an engine install drops the prefix. The text stays under 1,600 characters, and generate_skill_index.py --check fails when the committed file no longer matches the catalog.

When Apothem runs as a Claude Code plugin alone, no rule file loads at launch. The SessionStart hook then adds a short pointer to the bundled rules/ directory. If that directory holds apothem-rule-index.md, the pointer also tells the session to read the four core rules first, because nothing loads them in this posture (agnostic-posture, authority-inquiry, session-closure, context-management), and names the index, which says which rule to read for which task. The index text is not inlined.

When the project holds a knowledge base made by the knowledge-wiki skill, the SessionStart hook compares its fingerprint with the one recorded at the last baseline and, if they differ, adds one line that suggests the skill's drift mode (--mode drift --kb <path>). The fingerprint is the page count plus the newest log entry; the recorded copy is the newest baseline line in the base's log.md, which only the drift mode writes. The hook looks at four paths under the project root, in order: knowledge-base/, wiki/, docs/knowledge-base/ and docs/wiki/, and only a directory that holds both index.md and log.md. A project without one pays under a millisecond. With one, the hook reads at most the last 256 KiB of log.md and lists three flat directories; it never walks the tree and never writes. The line carries a fixed path, a date and two page counts, never text from the base. It stays silent when there is no base, no baseline, no change, or a file it cannot read, and it repeats each session until the drift mode records a new baseline. A base at any other path works in every skill mode but gets no session-start line.

Each context file is loaded by the dispatcher when its event fires and its context argument is registered in the settings file. The license comment line and the closing ## Bindings section are removed before the text reaches the assistant; both exist for maintainers and the conformity gate.

A guard that finds nothing injects nothing. A guard that fires injects only the ## Advisory section of its message, filled with what it found (the path, the identifier, the primitive), followed by Full procedure: <message-file>. The rest of the file is the full procedure, which the assistant reads only when it needs the detail. Guards judge "inside the project" against the git root above the tool call's working directory, never against the tree the hook is installed in.

Runtime contract

Every registered hook command meets the same contract, checked by tests/hooks/test_hook_contract.py on the plugin, the Claude Code engine install, Codex, Qwen Code, and the CodeBuddy install (the Kiro, Copilot, Cursor and Windsurf files are checked against their contracts by tests/hooks/test_native_hook_registrations.py and run as the vendors run them by tests/integration/test_native_hook_files_roundtrip.py, test_cursor_hooks_roundtrip.py and test_windsurf_hooks_roundtrip.py):

  • Runs from a plain checkout. Exit 0, and standard output is empty or one JSON object. No hook depends on a file's executable bit.
  • Registered once. Each event and matcher has one dispatcher entry per surface, listing every message it evaluates, so a guard never runs twice on one tool call.
  • Bounded. No emitted text exceeds 10,000 characters. The dependency guard speaks only when a write targets a dependency manifest or lockfile, and the dynamic-eval guard only when the content or command contains an evaluation or unsafe-deserialization primitive. The compaction advisory fires at most twice per compaction cycle.
  • Uses channels the harness delivers. PreToolUse guidance travels in hookSpecificOutput; the AskUserQuestion guard returns additionalContext, or permissionDecision: deny under the strict opt-in. Claude Code discards PreCompact and PostCompact output, so on Claude Code the post-compaction recovery context arrives with the SessionStart event whose source is compact.
  • Never runs project code. The bootstrap resolves its own installed tree and never takes a script from the opened project. Session-start guidance is read from the installed tree; the project directory is read only for plan and memory summaries and the knowledge-base drift check.
  • Fails open. A missing message file, a missing interpreter, or corrupt state yields exit 0 and a valid envelope.
VariableDefaultEffect
APOTHEM_HOOKS_DISABLEunset1, true, yes or on silences every dispatcher-routed hook and the engine install's conformity-gate hook
APOTHEM_SESSION_END_ENABLEDoff1, true, yes or on enables the session-end protocol on Stop. A Stop hook that returns context makes the assistant continue, so the protocol costs one extra turn per session; it never fires on a stop that already follows a hook-driven continuation
APOTHEM_SESSION_END_MIN_STOPS3Turn-ends before the session-end protocol may fire
APOTHEM_PROACTIVE_COMPACTION_ENABLEDon0, false, no or off silences the compaction advisory
APOTHEM_CONTEXT_WINDOW_TOKENSunsetContext-window size; with it (or Claude Code's CLAUDE_CODE_AUTO_COMPACT_WINDOW) the advisories fire at 50% and 75% of the window, otherwise at 120,000 and 250,000 tokens
APOTHEM_PROACTIVE_COMPACTION_TOOL_THRESHOLD18Tool calls per advisory window, used only when the context size cannot be measured
APOTHEM_PROACTIVE_COMPACTION_OUTPUT_THRESHOLD20000Tool-output bytes per advisory window, used only when the context size cannot be measured
APOTHEM_HOOK_STATE_DIRsee belowDirectory for per-session hook state

The compaction advisory measures the context from the newest usage record in the session transcript (input plus cached tokens), so it reflects what the model actually holds. A sharp drop in that size marks a compaction and restarts the two-advisory budget. Where no transcript is available it counts tool calls and output bytes instead.

Per-session hook state lives in a per-user directory created with mode 0700: APOTHEM_HOOK_STATE_DIR when set, else ${CLAUDE_PLUGIN_DATA}/state for the Claude Code plugin, else $XDG_STATE_HOME/apothem, else ~/.local/state/apothem (%LOCALAPPDATA%\apothem\state on Windows).

Generated Hook Message Inventory

NameDescriptionSource
postcompactPost-compaction Blind Bootstrap. Restore working state from durable files; conversation context is now compressed.src/apothem/hooks/messages/postcompact.md
posttooluse-proactive-compactionProactive-compaction tracker for PostToolUse tool calls.src/apothem/hooks/messages/posttooluse-proactive-compaction.md
precompactPre-compaction protocol. Externalize all in-conversation state BEFORE the harness compresses context.src/apothem/hooks/messages/precompact.md
pretooluse-askuserquestion-recommendedCall-time (Recommended)-marker guard for AskUserQuestion tool calls.src/apothem/hooks/messages/pretooluse-askuserquestion-recommended.md
pretooluse-bash-plan-guardPlans-Discipline write guard for Bash tool calls (companion to pretooluse-write-plan-guard.md).src/apothem/hooks/messages/pretooluse-bash-plan-guard.md
pretooluse-bashGit write guard: plan-internal isolation, numeric-prefix discipline, human-only authorship.src/apothem/hooks/messages/pretooluse-bash.md
pretooluse-conformity> Advisory: this hook reports; it does not block. Mechanical enforcement runs in CI.src/apothem/hooks/messages/pretooluse-conformity.md
pretooluse-dependency-guardDependency-manifest supply-chain guard (advisory).src/apothem/hooks/messages/pretooluse-dependency-guard.md
pretooluse-edit-header-guardSPDX-header inject guard for Edit tool calls.src/apothem/hooks/messages/pretooluse-edit-header-guard.md
pretooluse-editCodebase isolation (Edit scope).src/apothem/hooks/messages/pretooluse-edit.md
pretooluse-eval-guardDynamic-eval / unsafe-deserialization guard — advisory prompt-injection defense.src/apothem/hooks/messages/pretooluse-eval-guard.md
pretooluse-notebookeditCodebase isolation (notebook scope).src/apothem/hooks/messages/pretooluse-notebookedit.md
pretooluse-write-header-guardSPDX-header inject guard for Write tool calls.src/apothem/hooks/messages/pretooluse-write-header-guard.md
pretooluse-write-plan-guardPlans-Discipline write guard for Write / Edit / NotebookEdit tool calls.src/apothem/hooks/messages/pretooluse-write-plan-guard.md
pretooluse-writeCodebase isolation + frontmatter compliance.src/apothem/hooks/messages/pretooluse-write.md
sessionstartSession-start conformity posture.src/apothem/hooks/messages/sessionstart.md
skill-indexSkills by task. /apothem:name = manual, run only on request.src/apothem/hooks/messages/skill-index.md
stopSession-end protocol: externalize state; evaluate memory and artifacts.src/apothem/hooks/messages/stop.md

On this page