Write a custom Hook

Hooks are scripts DOS runs at lifecycle events — use them to enforce rules, sync state, or speak out loud without touching the Algorithm.

A hook is a script the harness runs at a lifecycle event. Register it once in ~/.claude/settings.json, fires every time the event matches. Hooks enforce conventions, sync state, and speak out loud without the Algorithm having to remember.

Hooks are not skills. Skills are invoked by Claude choosing to call them. Hooks are invoked by the harness, whether Claude noticed or not.

The five lifecycle events

EventFires whenTypical use
SessionStartA session bootsForce-load steering rules, warm caches, announce the session
UserPromptSubmitThe user sends a messageRate the last reply, classify the new request, inject context
PreToolUseClaude is about to call a toolBlock dangerous writes, require confirmation, log intent
PostToolUseA tool call finishedSync PRD state, run convention checks, refresh the dashboard
SessionEndThe session is endingMine learnings into MemPalace, flush dashboards

Every hook you write fits one of these five slots.

Where hooks register

Hooks go in ~/.claude/settings.json under the hooks key. Each event name holds an array:

{
  "hooks": {
    "PostToolUse": [
      { "type": "command", "command": "${DOS_DIR}/hooks/PRDSync.hook.ts" },
      { "type": "command", "command": "${DOS_DIR}/hooks/SentinelGuard.hook.ts" }
    ],
    "SessionEnd": [
      { "type": "command", "command": "${DOS_DIR}/hooks/MemPalaceLearn.hook.ts" }
    ]
  }
}

The harness runs each entry in order. A hook that fails to parse or exits non-zero is logged but doesn't block the session unless you explicitly return a blocking code.

The TypeScript hook signature

DOS hooks are Bun TypeScript scripts. They read a JSON event payload from stdin and write warnings to stderr:

#!/usr/bin/env bun

interface HookInput {
  tool_name: string;
  tool_input: {
    file_path?: string;
    command?: string;
  };
  tool_output?: string;
}

const input: HookInput = await Bun.stdin.json();

if (input.tool_name !== 'Write' && input.tool_name !== 'Edit') {
  process.exit(0);
}

// Do your thing — read files, run checks, sync state.

if (warnings.length > 0) {
  console.error(`MyHook: ${warnings.join(', ')}`);
}

process.exit(0);

Three rules the SentinelGuard hook demonstrates:

  1. Fast path out. If the event doesn't apply, process.exit(0) immediately. Hooks fire constantly; wasted work slows every tool call.
  2. Stderr for warnings. Stdout is reserved for machine output. Stderr is where humans and logs see messages.
  3. Always exit 0 for read-only hooks. A non-zero exit blocks the tool call. Reserve that for emergencies.

The SentinelGuard reference

SentinelGuard is the simplest non-mutating hook in DOS. PostToolUse, walks up from the edited file to find .sentinel/conventions.json, loads regex rules, prints violations to stderr. Never modifies, never blocks.

Read it at ~/.claude/hooks/SentinelGuard.hook.ts. 120 lines. It caps directory walks at 10 levels, silently exits 0 on any filesystem error, and reports violations by rule ID.

Read-only vs mutating

Read-only hooks observe and report. SentinelGuard warns on violations; PRDSync reads PRD.md and writes work.json.

Mutating hooks change state. MemPalaceLearn writes session data on SessionEnd. A steering rule injector might rewrite CLAUDE.md at SessionStart.

The rule: if a read-only hook can do the job, use a read-only hook. Mutating hooks compound across sessions and are painful to debug.

Never mutate PRD.md from a hook

PRD.md is the Algorithm's system of record. Only the Algorithm writes it. Read the PRD from your hook; project state into work.json or elsewhere.

Step-by-step

Build a hook

From empty file to registered and firing.

  1. Pick the event. Want it on every save? PostToolUse filtered to Write/Edit.
  2. Write the file. Start from the SentinelGuard shape. Filter out events you don't care about, do the minimum work, exit 0.
  3. Save it. Land it at ~/.claude/hooks/MyHook.hook.ts. Bun handles the shebang; no chmod needed.
  4. Register it. Append an entry to the right event array in ~/.claude/settings.json.
  5. Trigger it. Make a Write/Edit call. Check stderr in the harness logs.

Troubleshoot

  • Never fires. Event name spelling in settings.json. Case matters.
  • Fires but nothing visible. Warnings go to stderr; the harness may suppress them in the UI. Check raw session logs.
  • Blocks a tool call. You returned non-zero. Change it to process.exit(0).
  • Slow. Add an early-return filter. Most hooks should finish in under 100ms.

Next

Was this page helpful?