Hook System
How Hooks Work
Hooks are Python scripts that run automatically when Claude Code emits lifecycle events. They are configured in ~/.claude/settings.json and execute outside of Claude's context -- Claude does not see hook code or control when hooks fire.
Each hook receives a JSON payload on stdin with event-specific data (session ID, transcript path, tool name). Hooks communicate with Cognova by calling its REST API through a shared client library.
Hook Configuration
Hooks are defined in settings.json under the hooks key:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "python3 ~/.claude/hooks/session-start.py" }
]
}
]
}
}
Each event type maps to an array of hook groups. A hook group can optionally include a matcher field to filter by tool name (used for PreToolUse and PostToolUse events).
Lifecycle Events
SessionStart -----> Active Session -----> Stop -----> SessionEnd
| |
PreToolUse PreCompact
PostToolUse (if context full)
session-start.py
Event: SessionStart -- fires when a Claude session opens.
What it does:
- Logs the session start event to the analytics API
- Calls
GET /api/memory/contextto fetch high-relevance memories - Prints the formatted memory context to stdout, which Claude receives as additional context
Output behavior: If memories exist, Claude sees a ## Session Memory block with past decisions, facts, preferences, and patterns grouped by type. If no memories exist (first session), Claude sees an onboarding prompt instructing it to ask about the user's background and store each response.
First session output:
## Session Memory -- ONBOARDING REQUIRED
No memories loaded. This is likely a first session.
IMPORTANT: Before doing anything else, introduce yourself briefly and then
ask the user the following questions in a single message:
1. What do you do? (role, profession, background)
2. What kind of projects do you work on?
3. What are you hoping to use Cognova for?
4. Any tools, conventions, or preferences you're particular about?
Returning session output:
## Session Memory
The following memories were loaded from previous sessions.
### Preferences
- User is a fullstack developer working primarily with TypeScript and Nuxt
- Prefers Tailwind CSS over CSS modules
### Decisions
- Using Caddy instead of Nginx for reverse proxy
- PostgreSQL for all persistent data, no SQLite
stop-extract.py
Event: Stop -- fires after Claude finishes a response.
What it does:
- Reads the conversation transcript path from the hook input
- Sends the transcript to
POST /api/memory/extract - The server-side extractor uses a separate Claude instance to identify memories worth preserving
- Extracted memories are stored in the database automatically
Timeout: 60 seconds (configured in settings.json), since extraction involves an LLM call.
Guard: If stop_hook_active is set in the input (meaning Claude is already continuing from a stop hook), the script exits immediately to prevent infinite loops.
The stop hook runs asynchronously -- it does not block Claude from accepting the next user message. Memory extraction happens in the background.
pre-compact.py
Event: PreCompact -- fires before Claude's context window is compacted (summarized).
What it does:
- Logs the compaction event (noting whether it was manual or automatic)
- Reads the full transcript from the provided path
- Sends it to
POST /api/memory/extractfor memory extraction
This is a critical hook. Context compaction discards conversation detail in favor of a summary. Without this hook, decisions and facts discussed early in a long session would be lost forever.
The pre-compact hook always exits with code 0 to avoid blocking compaction. If memory extraction fails, the compaction still proceeds -- but a warning is logged to stderr.
log-event.py
Event: PreToolUse and PostToolUse -- fires around tool executions.
What it does:
- Reads the tool name and input from the hook payload
- Logs the event to
POST /api/hooks/eventswith timing data - If wrapping another hook, runs that hook and captures its exit code
- Records whether the action was blocked (exit code 2) and the reason
Matchers: Configured to fire on specific tool patterns:
| Event | Matcher | Triggers On |
|---|---|---|
PreToolUse | Edit|Write|NotebookEdit | File modifications |
PreToolUse | Bash | Shell commands |
PostToolUse | `` (empty) | All tool completions |
The empty matcher on PostToolUse means it fires for every tool call, providing complete analytics coverage.
Logged payload:
{
"eventType": "PreToolUse",
"toolName": "Bash",
"sessionId": "abc-123",
"projectDir": "/home/user/project",
"durationMs": 45,
"blocked": false,
"hookScript": "log-event.py"
}
session-end.py
Event: SessionEnd -- fires when a Claude session closes.
What it does: Logs the session end event to the analytics API. This is a lightweight hook that records the session close for duration tracking and usage analytics.
Shared Hook Client
All hooks import from hooks/lib/hook_client.py, which provides three functions:
| Function | Purpose |
|---|---|
log_event(...) | POST to /api/hooks/events with event metadata |
extract_memories(...) | POST to /api/memory/extract with transcript data |
get_memory_context(...) | GET from /api/memory/context for session injection |
The client authenticates using the same .api-token file that skills use. It includes aggressive timeouts (2-5 second connect, 5-30 second max) to avoid blocking Claude if the API is slow. All failures are silent -- hooks log errors to stderr but never prevent Claude from operating.
# Every hook follows this pattern
from hook_client import log_event, read_stdin_json
def main():
hook_input = read_stdin_json()
log_event(event_type='EventName', event_data=hook_input, hook_script='name.py')
# ... event-specific logic
Exit Code Conventions
| Code | Meaning |
|---|---|
0 | Success -- allow the action to proceed |
1 | Error -- logged but does not block |
2 | Block -- prevents the tool action (used by guard hooks) |
The log-event.py wrapper preserves the exit code of any wrapped hook, so blocking behavior propagates correctly through the logging layer.