Skip to content

Hook Sequence Diagrams

Hook Lifecycle

Every ctx hook is a Go binary invoked by Claude Code at one of three lifecycle events: PreToolUse (before a tool runs, can block), PostToolUse (after a tool completes), or UserPromptSubmit (on every user prompt, before any tools run). Hooks receive JSON on stdin and emit JSON or plain text on stdout.

This page documents the execution flow of every hook as a sequence diagram.


PreToolUse Hooks

These fire before a tool executes. They can block, gate, or inject context.

context-load-gate

Matcher: .* (all tools)

Injects the full context packet on first tool use of a session. One-shot per session.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as context-load-gate
    participant State as .context/state/
    participant Ctx as .context/ files
    participant Git as git log

    CC->>Hook: stdin {command, session_id}
    Hook->>Hook: Check initialized
    alt not initialized
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: Check paused
    alt paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Check ctx-loaded-{session} marker
    alt marker exists
        Hook-->>CC: (silent exit, already fired)
    end
    Hook->>State: Create marker (one-shot guard)
    Hook->>State: Prune stale session files
    loop Each file in ReadOrder
        alt GLOSSARY or TASK
            Note over Hook: Skip (Task mentioned in footer only)
        else DECISION or LEARNING
            Hook->>Ctx: Extract index table only
        else other files
            Hook->>Ctx: Read full content
        end
        Hook->>Hook: Estimate tokens per file
    end
    Hook->>Git: Detect changes since last session
    Hook->>Hook: Build injection (files + changes + token counts)
    Hook-->>CC: JSON {additionalContext: injection}
    Hook->>Hook: Send webhook (metadata only)
    Hook->>State: Write oversize flag if tokens > threshold

block-non-path-ctx

Matcher: Bash

Blocks ./ctx, go run ./cmd/ctx, or absolute-path ctx invocations. Constitutionally enforced.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as block-non-path-ctx
    participant Tpl as Message Template

    CC->>Hook: stdin {command, session_id}
    Hook->>Hook: Extract command
    alt command empty
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: Test regex: relative-path, go-run, absolute-path
    alt no match
        Hook-->>CC: (silent exit)
    end
    alt absolute-path + test exception
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, variant, fallback)
    Hook-->>CC: JSON {decision: BLOCK, reason + constitution suffix}
    Hook->>Hook: NudgeAndRelay(message)

qa-reminder

Matcher: Bash

Gate nudge before any git command. Reminds agent to lint/test.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as qa-reminder
    participant Tpl as Message Template

    CC->>Hook: stdin {command, session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: Check command contains "git"
    alt no git command
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, gate, fallback)
    Hook->>Hook: AppendDir(message)
    Hook-->>CC: JSON {additionalContext: QA gate}
    Hook->>Hook: Relay(message)

specs-nudge

Matcher: EnterPlanMode

Nudges agent to save plans/specs when new implementation detected.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as specs-nudge
    participant Tpl as Message Template

    CC->>Hook: stdin {command, session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, nudge, fallback)
    Hook->>Hook: AppendDir(message)
    Hook-->>CC: JSON {additionalContext: specs nudge}
    Hook->>Hook: Relay(message)

PostToolUse Hooks

These fire after a tool completes. They observe, nudge, and track state.

post-commit

Matcher: Bash

Fires after git commit (not amend). Nudges for context capture and checks version drift.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as post-commit
    participant Tpl as Message Template

    CC->>Hook: stdin {command, session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: Regex: command contains "git commit"?
    alt not a git commit
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: Regex: command contains "--amend"?
    alt is amend
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, nudge, fallback)
    Hook->>Hook: AppendDir(message)
    Hook-->>CC: JSON {additionalContext: post-commit nudge}
    Hook->>Hook: Relay(message)
    Hook->>Hook: CheckVersionDrift()

check-task-completion

Matcher: Edit, Write

Configurable-interval nudge after edits. Per-session counter resets after firing.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-task-completion
    participant State as .context/state/
    participant RC as .ctxrc
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>RC: Read task nudge interval
    alt interval <= 0 (disabled)
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Read per-session counter
    Hook->>Hook: Increment counter
    alt counter < interval
        Hook->>State: Write counter
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Reset counter to 0
    Hook->>Tpl: LoadMessage(hook, nudge, fallback)
    Hook-->>CC: JSON {additionalContext: task nudge}
    Hook->>Hook: Relay(message)

UserPromptSubmit Hooks

These fire on every user prompt, before any tools run. They perform health checks, track state, and nudge for housekeeping.

check-context-size

Adaptive context window monitoring. Fires checkpoints, window warnings, and billing alerts based on prompt count and token usage.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-context-size
    participant State as .context/state/
    participant Session as Session JSONL
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized
    Hook->>Hook: Read input, resolve session ID
    Hook->>Hook: Check paused
    alt paused
        Hook-->>CC: Pause acknowledgment message
    end
    Hook->>State: Increment session prompt counter
    Hook->>Session: Read token info (tokens, model, window)

    rect rgb(255, 240, 240)
        Note over Hook: Billing check (independent, never suppressed)
        alt tokens >= billing threshold (one-shot)
            Hook->>Tpl: LoadMessage(hook, billing, vars)
            Hook-->>CC: Billing warning nudge box
            Hook->>Hook: NudgeAndRelay(billing message)
        end
    end

    Hook->>State: Check wrap-up marker
    alt wrapped up recently (< 2h)
        Hook->>State: Write stats (event: suppressed)
        Hook-->>CC: (silent exit)
    end

    rect rgb(240, 248, 255)
        Note over Hook: Adaptive frequency check
        alt count > 30 and count % 3 == 0
            Note over Hook: High frequency trigger
        else count > 15 and count % 5 == 0
            Note over Hook: Medium frequency trigger
        else
            Hook->>State: Write stats (event: silent)
            Hook-->>CC: (silent exit)
        end
    end

    alt context window >= 80%
        Hook->>Tpl: LoadMessage(hook, window, vars)
        Hook-->>CC: Window warning nudge box
        Hook->>Hook: NudgeAndRelay(window message)
    else checkpoint trigger
        Hook->>Tpl: LoadMessage(hook, checkpoint)
        Hook-->>CC: Checkpoint nudge box
        Hook->>Hook: NudgeAndRelay(checkpoint message)
    end
    Hook->>State: Write session stats

check-ceremonies

Daily check for /ctx-remember and /ctx-wrap-up usage in recent journal entries.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-ceremonies
    participant State as .context/state/
    participant Journal as Journal files
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Check daily throttle marker
    alt throttled
        Hook-->>CC: (silent exit)
    end
    Hook->>Journal: Read recent files (lookback window)
    alt no journal files
        Hook-->>CC: (silent exit)
    end
    Hook->>Journal: Scan for /ctx-remember and /ctx-wrap-up
    alt both ceremonies present
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, variant, fallback)
    Note over Hook: variant: both | remember | wrapup
    Hook-->>CC: Nudge box (missing ceremonies)
    Hook->>Hook: NudgeAndRelay(message)
    Hook->>State: Touch throttle marker

check-freshness

Daily check for technology-dependent constants that may need review.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-freshness
    participant State as .context/state/
    participant FS as Filesystem
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Check daily throttle marker
    alt throttled
        Hook-->>CC: (silent exit)
    end
    Hook->>FS: Stat tracked files (5 source files)
    alt all files modified within 6 months
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, stale, {StaleFiles})
    Hook-->>CC: Nudge box (stale file list + review URL)
    Hook->>Hook: NudgeAndRelay(message)
    Hook->>State: Touch throttle marker

check-journal

Daily check for unexported sessions and unenriched journal entries.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-journal
    participant State as .context/state/
    participant Journal as Journal dir
    participant Claude as Claude projects dir
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Check daily throttle marker
    alt throttled
        Hook-->>CC: (silent exit)
    end
    Hook->>Journal: Check dir exists
    Hook->>Claude: Check dir exists
    alt either dir missing
        Hook-->>CC: (silent exit)
    end
    Hook->>Journal: Get newest entry mtime
    Hook->>Claude: Count .jsonl files newer than journal
    Hook->>Journal: Count unenriched entries
    alt unexported == 0 and unenriched == 0
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, variant, {counts})
    Note over Hook: variant: both | unexported | unenriched
    Hook-->>CC: Nudge box (counts)
    Hook->>Hook: NudgeAndRelay(message)
    Hook->>State: Touch throttle marker

check-knowledge

Daily check for knowledge file entry/line counts exceeding configured thresholds.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-knowledge
    participant State as .context/state/
    participant Ctx as .context/ files
    participant RC as .ctxrc
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Check daily throttle marker
    alt throttled
        Hook-->>CC: (silent exit)
    end
    Hook->>RC: Read thresholds (decisions, learnings, conventions)
    alt all thresholds disabled (0)
        Hook-->>CC: (silent exit)
    end
    Hook->>Ctx: Parse DECISIONS.md entry count
    Hook->>Ctx: Parse LEARNINGS.md entry count
    Hook->>Ctx: Count CONVENTIONS.md lines
    Hook->>Hook: Compare against thresholds
    alt all within limits
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, warning, {FileWarnings})
    Hook-->>CC: Nudge box (file warnings)
    Hook->>Hook: NudgeAndRelay(message)
    Hook->>State: Touch throttle marker

check-map-staleness

Daily check for architecture map age and relevant code changes.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-map-staleness
    participant State as .context/state/
    participant Tracking as map-tracking.json
    participant Git as git log
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Check daily throttle marker
    alt throttled
        Hook-->>CC: (silent exit)
    end
    Hook->>Tracking: Read map-tracking.json
    alt missing, invalid, or opted out
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: Parse LastRun date
    alt map not stale (< N days)
        Hook-->>CC: (silent exit)
    end
    Hook->>Git: Count commits touching internal/ since LastRun
    alt no relevant commits
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, stale, {date, count})
    Hook-->>CC: Nudge box (last refresh + commit count)
    Hook->>Hook: NudgeAndRelay(message)
    Hook->>State: Touch throttle marker

check-memory-drift

Per-session check for MEMORY.md changes since last sync.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-memory-drift
    participant State as .context/state/
    participant Mem as memory.Discover
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Check session tombstone
    alt already nudged this session
        Hook-->>CC: (silent exit)
    end
    Hook->>Mem: DiscoverMemoryPath(projectRoot)
    alt auto memory not active
        Hook-->>CC: (silent exit)
    end
    Hook->>Mem: HasDrift(contextDir, sourcePath)
    alt no drift
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, nudge, fallback)
    Hook-->>CC: Nudge box (drift reminder)
    Hook->>Hook: NudgeAndRelay(message)
    Hook->>State: Touch session tombstone

check-persistence

Tracks context file modification and nudges when edits happen without persisting context. Adaptive threshold based on prompt count.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-persistence
    participant State as .context/state/
    participant Ctx as .context/ files
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Read persistence state {Count, LastNudge, LastMtime}
    alt first prompt (no state)
        Hook->>State: Initialize state {Count:1, LastNudge:0, LastMtime:now}
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: Increment Count
    Hook->>Ctx: Get current context mtime
    alt context modified since LastMtime
        Hook->>State: Reset LastNudge = Count, update LastMtime
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: sinceNudge = Count - LastNudge
    Hook->>Hook: PersistenceNudgeNeeded(Count, sinceNudge)?
    alt threshold not reached
        Hook->>State: Write state
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, nudge, vars)
    Hook-->>CC: Nudge box (prompt count, time since last persist)
    Hook->>Hook: NudgeAndRelay(message)
    Hook->>State: Update LastNudge = Count, write state

check-reminders

Per-prompt check for due reminders. No throttle.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-reminders
    participant Store as Reminders store
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>Store: ReadReminders()
    alt load error
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: Filter by due date (After <= today)
    alt no due reminders
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, reminders, {list})
    Hook-->>CC: Nudge box (reminder list + dismiss hints)
    Hook->>Hook: NudgeAndRelay(message)

check-resources

Checks system resources (memory, swap, disk, load). Fires on every prompt. No initialization required.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-resources
    participant Sys as sysinfo
    participant Tpl as Message Template

    CC->>Hook: stdin {command, session_id}
    Hook->>Hook: HookPreamble (parse input, check pause)
    alt paused
        Hook-->>CC: (silent exit)
    end
    Hook->>Sys: Collect snapshot (memory, swap, disk, load)
    Hook->>Sys: Evaluate thresholds per metric
    alt max severity < Danger
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: Filter alerts to Danger level only
    Hook->>Hook: Build alertMessages from danger alerts
    Hook->>Tpl: LoadMessage(hook, alert, {alertMessages}, fallback)
    Hook-->>CC: Nudge box (danger alerts)
    Hook->>Hook: NudgeAndRelay(message)

check-version

Daily binary-vs-plugin version comparison with piggybacked key rotation check.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-version
    participant State as .context/state/
    participant Config as Binary + Plugin version
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Check daily throttle marker
    alt throttled
        Hook-->>CC: (silent exit)
    end
    Hook->>Config: Read binary version
    alt dev build
        Hook->>State: Touch throttle
        Hook-->>CC: (silent exit)
    end
    Hook->>Config: Read plugin version
    alt plugin version not found or parse error
        Hook->>State: Touch throttle
        Hook-->>CC: (silent exit)
    end
    Hook->>Hook: Compare major.minor
    alt versions match
        Hook->>State: Touch throttle
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, mismatch, {versions})
    Hook-->>CC: Nudge box (version mismatch)
    Hook->>Hook: NudgeAndRelay(message)
    Hook->>State: Touch throttle
    Hook->>Hook: CheckKeyAge() (piggybacked)

heartbeat

Silent per-prompt pulse. Tracks prompt count, context modification, and token usage. The agent never sees this hook's output.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as heartbeat
    participant State as .context/state/
    participant Ctx as .context/ files
    participant Notify as Webhook + EventLog

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Increment heartbeat counter
    Hook->>Ctx: Get latest context file mtime
    Hook->>State: Compare with last recorded mtime
    Hook->>State: Update mtime record
    Hook->>State: Read session token info
    Hook->>Notify: Send heartbeat notification
    Hook->>Notify: Append to event log
    Hook->>State: Write heartbeat log entry
    Note over Hook: No stdout - agent never sees this

Project-Local Hooks

These hooks are configured in settings.local.json and are not shipped with ctx. They are specific to individual developer setups.

block-dangerous-commands

Lifecycle: PreToolUse. Matcher: Bash

Blocks dangerous shell patterns (sudo, git push, cp to bin). No initialization or pause checks: always active.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as block-dangerous-commands
    participant Tpl as Message Template

    CC->>Hook: stdin {command, session_id}
    Hook->>Hook: Extract command
    alt command empty
        Hook-->>CC: (silent exit)
    end
    Note over Hook: Cascade: first matching regex wins
    Hook->>Hook: Test MidSudo regex
    alt match
        Hook->>Hook: variant = sudo
    end
    Hook->>Hook: Test MidGitPush regex (if no variant)
    alt match
        Hook->>Hook: variant = git-push
    end
    Hook->>Hook: Test CpMvToBin regex (if no variant)
    alt match
        Hook->>Hook: variant = cp-to-bin
    end
    Hook->>Hook: Test InstallToLocalBin regex (if no variant)
    alt match
        Hook->>Hook: variant = install-to-bin
    end
    alt no variant matched
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, variant, fallback)
    Hook-->>CC: JSON {decision: BLOCK, reason}
    Hook->>Hook: NudgeAndRelay(message)

check-backup-age

Lifecycle: UserPromptSubmit.

Daily check for SMB mount and backup freshness.

sequenceDiagram
    participant CC as Claude Code
    participant Hook as check-backup-age
    participant State as .context/state/
    participant FS as Filesystem
    participant Tpl as Message Template

    CC->>Hook: stdin {session_id}
    Hook->>Hook: Check initialized + HookPreamble
    alt not initialized or paused
        Hook-->>CC: (silent exit)
    end
    Hook->>State: Check daily throttle marker
    alt throttled
        Hook-->>CC: (silent exit)
    end
    Hook->>FS: Check SMB mount (if env var set)
    Hook->>FS: Check backup marker file age
    alt no warnings
        Hook-->>CC: (silent exit)
    end
    Hook->>Tpl: LoadMessage(hook, warning, {Warnings})
    Hook-->>CC: Nudge box (warnings)
    Hook->>Hook: NudgeAndRelay(message)
    Hook->>State: Touch throttle marker

Throttling Summary

Hook Lifecycle Throttle Type Scope
context-load-gate PreToolUse One-shot marker Per session
block-non-path-ctx PreToolUse None Every match
qa-reminder PreToolUse None Every git command
specs-nudge PreToolUse None Every prompt
post-commit PostToolUse None Every git commit
check-task-completion PostToolUse Configurable interval Per session
check-context-size UserPromptSubmit Adaptive counter Per session
check-ceremonies UserPromptSubmit Daily marker Once per day
check-freshness UserPromptSubmit Daily marker Once per day
check-journal UserPromptSubmit Daily marker Once per day
check-knowledge UserPromptSubmit Daily marker Once per day
check-map-staleness UserPromptSubmit Daily marker Once per day
check-memory-drift UserPromptSubmit Session tombstone Once per session
check-persistence UserPromptSubmit Adaptive counter Per session
check-reminders UserPromptSubmit None Every prompt
check-resources UserPromptSubmit None Every prompt
check-version UserPromptSubmit Daily marker Once per day
heartbeat UserPromptSubmit None Every prompt
block-dangerous-commands PreToolUse * None Every match
check-backup-age UserPromptSubmit * Daily marker Once per day

* Project-local hook (settings.local.json), not shipped with ctx.

State File Reference

All state files live in .context/state/.

File Pattern Hook Purpose
ctx-loaded-{session} context-load-gate One-shot injection marker
ctx-paused-{session} (all) Session pause marker
ctx-wrapped-up check-context-size Suppress nudges after wrap-up (2h expiry)
freshness-checked check-freshness Daily throttle
backup-reminded check-backup-age Daily throttle
ceremony-reminded check-ceremonies Daily throttle
journal-reminded check-journal Daily throttle
knowledge-reminded check-knowledge Daily throttle
map-staleness-reminded check-map-staleness Daily throttle
version-checked check-version Daily throttle
memory-drift-nudged-{session} check-memory-drift Per-session tombstone
ctx-context-count-{session} check-context-size Prompt counter
stats-{session}.jsonl check-context-size Session stats log
persist-{session} check-persistence Counter + mtime state
ctx-task-count-{session} check-task-completion Prompt counter
heartbeat-count-{session} heartbeat Prompt counter
heartbeat-mtime-{session} heartbeat Last context mtime