Hooks are event-driven scripts that fire at specific points in the Claude Code lifecycle. They are the nervous system of Cortex — sensing events, injecting context, and triggering side effects without requiring explicit user commands.
Claude Code exposes eight hook points. Cortex uses six of them actively, with two reserved as placeholders for future agent coordination.
SESSION START
|
v
+------------------+
| 1. SessionStart | session-start.sh
| git sync | cortex recompile
| load state | activate session
+------------------+
|
v
USER ENTERS PROMPT
|
v
+----------------------+
| 2. UserPromptSubmit | prompt-router.sh
| domain triage | exit detection
| forced eval | duo inbox check
+----------------------+
|
v
CLAUDE SELECTS A TOOL
|
v
+------------------+
| 3. PreToolUse | strategic-compact
| check context | suggest compaction
| gate actions |
+------------------+
|
v
TOOL EXECUTES
|
v
+------------------+
| 4. PostToolUse | observe.sh
| log tool call | record patterns
| async write |
+------------------+
|
| (repeat 3-4 for each tool in the response)
|
v
CONTEXT COMPACTION TRIGGERED
|
v
+------------------+
| 5. PreCompact | pre-compact.sh
| snapshot state| preserve decisions
| save markers |
+------------------+
|
v
SESSION ENDS (via /bye or timeout)
|
v
+------------------+ +--------------------+
| 6. Stop | | 7. SubagentStart | (placeholder)
| analyze.py | | 8. SubagentStop | (placeholder)
| archive | +--------------------+
+------------------+
When: Fires once when Claude Code launches a new session.
What it does:
- Git sync (optional). If your
~/.claude/is version-controlled, pulls latest to keep hooks, skills, and rules current across devices. - Cortex recompile. Checks if
cortex/registry.mdis newer than the compiled registry. If so, runscortex/compile.jsto rebuild the routing index. - Activate session. Reads
sessions/CURRENT.mdand feeds it into the Synergatis agent, which presents three task options by effort level.
Example output:
[session-start] Git sync: 2 files changed
[session-start] Cortex registry recompiled (51 capabilities)
[session-start] Session loaded: 6 open tasks, last active 2h ago
Key detail: This hook runs synchronously. The session does not begin until git sync and cortex recompile finish. This ensures Claude always operates on the latest configuration, even when switching between machines.
When: Fires on every user prompt, before Claude begins processing.
What it does:
-
Domain triage. Scans the prompt for domain-specific keywords using pattern lists. When it detects legal terms (prothesmia, dikastirio, klironomia), React Native imports, genomic markers, or other domain signals, it suggests loading the corresponding domain rules file. This keeps domain rules out of passive context until they are actually needed, saving ~550 tokens per turn.
-
Exit detection. Watches for exit signals: "bye", "done", "save", "telos", and Greek equivalents. When detected, injects the EXIT PROTOCOL instructions that trigger the
/byeconsolidation skill. -
Forced eval. Maintains a turn counter. Every 5 turns, injects a reflection snippet from
hooks/forced-eval-snippet.txt(809 bytes) that forces Claude to pause and evaluate whether its current approach is productive or stuck in a retry loop. -
Duo inbox check. If the duo MCP is active (cross-machine coordination), checks for unread messages from the other Claude instance and injects
[DUO: N unread]into the prompt context.
Example output (domain triage):
[prompt-router] Keywords detected: "prothesmia", "eirinodikio"
[prompt-router] Suggest loading: domain-rules/legal-el.md
Example output (exit detection):
[prompt-router] Exit signal detected: "bye"
[prompt-router] Injecting EXIT PROTOCOL
Example output (forced eval):
[prompt-router] Turn 10/5 — injecting forced evaluation
When: Fires before each tool call Claude makes.
What it does:
Monitors context window pressure and detects phase transitions in the work. When conditions suggest compaction would be beneficial, it emits a suggestion — it does not compact automatically.
Triggers for suggestion:
- Context usage exceeds ~80% capacity
- Phase transition detected (research to implementation, exploration to execution)
- Large intermediate results accumulated (search outputs, file reads no longer needed)
The suggestion is advisory. The user decides whether to compact. This respects the principle that compaction is lossy — it discards intermediate context that might still be useful.
Example output:
[strategic-compact] Context pressure: HIGH (~85%)
[strategic-compact] Detected: 3 large Grep results no longer referenced
[strategic-compact] Recommendation: compact before starting implementation phase
What it does NOT do: It does not block tool execution. It does not auto-compact. It does not trigger on every tool call — only when heuristics indicate compaction is worth considering.
When: Fires after each tool call completes.
What it does:
Logs the tool call to a JSONL observation file. Each entry records:
{
"timestamp": "2026-03-04T14:22:31Z",
"tool": "Bash",
"duration_ms": 342,
"success": true,
"prev_tool": "Read",
"prev2_tool": "Grep",
"session": "2026-03-04-a"
}This is the input to the learning loop. The observer runs asynchronously (run_in_background) so it does not slow down tool execution. It writes to the homunculus directory where analyze.py picks it up.
The observer records tool chains (sequences of 3 consecutive tools) because patterns in tool sequencing reveal workflow habits that can be optimized. For example, if Claude consistently runs Grep -> Read -> Edit for a certain class of task, the learning loop can promote that as an instinct.
Key constraint: The observer is read-only with respect to Claude's behavior. It logs but does not modify, gate, or redirect tool calls. Observation is passive.
When: Fires when context compaction is about to execute (either manual via /compact or automatic).
What it does:
Takes a snapshot of critical state before compaction destroys intermediate context:
- Decision log. Extracts any decisions made during the session (file paths, architectural choices, rejected alternatives) and writes them to a temporary marker file.
- File manifest. Records which files were modified during the session, with a one-line summary of each change.
- Task state. Snapshots the current state of
CURRENT.mdso post-compaction Claude knows where things stand. - Healthcheck. Verifies claude-mem is reachable and CURRENT.md is writable before allowing compaction to proceed.
This hook exists because compaction is the most dangerous routine operation in Claude Code. Without PreCompact, a compaction can erase the "why" behind decisions while preserving only the "what" — leaving post-compaction Claude unable to reason about tradeoffs that were already evaluated.
Example output:
[pre-compact] Snapshot saved: 3 decisions, 5 modified files
[pre-compact] CURRENT.md state preserved
[pre-compact] claude-mem: reachable (689 observations)
[pre-compact] Ready for compaction
When: Fires when the session ends (clean exit or timeout).
What it does:
- Runs
analyze.pyagainst the observation log to extract patterns from the session. - Archives the session's observation data.
- Updates instinct files with new or reinforced patterns.
This is the bridge between observation (hook 4) and the learning loop. See docs/learning-loop/README.md for the full pipeline.
When: Would fire when Claude spawns a sub-agent (Explore, specialist).
Status: Placeholder. Not yet implemented. Intended for future use in agent coordination — logging which agents are spawned, with what context, and for what purpose. Would feed into the learning loop to optimize agent delegation patterns.
When: Would fire when a sub-agent completes its task.
Status: Placeholder. Not yet implemented. Intended to capture agent results, execution time, and success/failure for the learning loop. Would also support the Maclink architecture where agents may run across machines.
A hook is a shell script (or any executable) placed in ~/.claude/hooks/ and registered in Claude Code's hook configuration. The basic structure:
#!/usr/bin/env bash
# Hook: my-custom-hook
# Event: PostToolUse
# Description: What this hook does
set -euo pipefail
# Access hook context via environment variables
# CLAUDE_TOOL_NAME - the tool that was called
# CLAUDE_SESSION_ID - current session identifier
# Your logic here
echo "[my-hook] Processing ${CLAUDE_TOOL_NAME}"
# Output goes to Claude's context (stdout)
# Errors go to debug log (stderr)Hooks are registered in the Claude Code settings under the appropriate event. Each event can have multiple hooks; they execute in registration order.
{
"hooks": {
"PostToolUse": [
{ "command": "bash ~/.claude/hooks/observe.sh" },
{ "command": "bash ~/.claude/hooks/my-custom-hook.sh" }
]
}
}-
Keep hooks fast. Synchronous hooks (SessionStart, PreCompact) block Claude. Target under 500ms. Use
run_in_backgroundfor anything slow. -
Stdout is context injection. Whatever your hook prints to stdout gets injected into Claude's context. Be concise — every byte costs tokens.
-
Stderr is debug. Use stderr for logging that should not enter Claude's context window.
-
Fail gracefully. A crashing hook should not break the session. Use
set -euo pipefailbut wrap risky operations in conditionals. -
Respect the lifecycle. Do not duplicate what another hook does. Check the diagram above to understand what fires when.
-
Test in isolation. Run your hook script directly with mock environment variables before registering it:
CLAUDE_TOOL_NAME=Bash CLAUDE_SESSION_ID=test bash ~/.claude/hooks/my-hook.sh