# Claude Code Hooks

Hooks for extending Claude Code behavior in arcforge.

## Structure

```
hooks/
├── hooks.json              # Hook configuration
├── run-hook.cmd            # Bash dispatcher
├── README.md
├── inject-skills/          # Injects arc-using skill at session start
│   ├── main.sh
│   └── README.md
├── arc-guard/              # Blocks unsafe ops (raw git merge / arcforge loop in epic worktrees, research scope violations)
│   ├── main.js
│   └── README.md
├── arc-remind/             # Post-action nudges: PR boundary, skill-shipping eval, main-branch warning
│   ├── main.js
│   └── README.md
├── sdd-ledger-guard/       # Blocks decision-ledger append-only violations and status forgery
│   ├── main.js
│   └── README.md
├── sdd-ratify-guard/       # Blocks `arcforge ratify` in unattended loop context
│   ├── main.js
│   └── README.md
├── quality-check/          # Auto-format & type-check on Edit
│   ├── main.js
│   ├── prettier.js
│   ├── typescript.js
│   └── README.md
├── compact-suggester/      # Suggests /compact at tool call thresholds
│   ├── main.js
│   └── README.md
├── pre-compact/            # Marks state before context compaction
│   ├── main.js
│   └── README.md
├── session-tracker/        # Session persistence
│   ├── inject-context.js   # Context injection at session start
│   ├── start.js
│   ├── end.js
│   ├── session.json.template
│   └── README.md
├── user-message-counter/   # Counts user prompts
│   ├── main.js
│   └── README.md
└── observe/                # Tool call observation
    └── main.js
```

## Active Hooks

### SessionStart

| Hook | Trigger | Description |
|------|---------|-------------|
| inject-skills | startup, resume, clear, compact | Injects arc-using skill content |
| session-tracker/inject-context | startup, resume, clear | Loads previous session context |
| session-tracker/start | startup, resume, clear | Resets counters, initializes session |

> **Why inject-skills includes `compact` but the others don't:**
> `inject-skills` sets `ARCFORGE_ROOT` via env file, which must persist through compaction.
> `inject-context` injects instincts into Claude's context, which gets rebuilt during compaction anyway.
> `session-tracker/start` creates session tracking state that shouldn't be re-initialized on compact.

### UserPromptSubmit

| Hook | Trigger | Description |
|------|---------|-------------|
| user-message-counter | All | Counts user messages for session evaluation |

### PreToolUse

| Hook | Trigger | Description |
|------|---------|-------------|
| arc-guard | Bash, Edit, Write | Blocking guard: denies raw `git merge`/`arcforge loop` inside epic worktrees and research-config scope violations (fail-open on errors) |
| sdd-ledger-guard | Edit, Write | Blocking guard: denies decision-ledger append-only violations and any attempt to mint `status: accepted` via Edit/Write |
| sdd-ratify-guard | Bash | Blocking guard: denies `arcforge ratify` while the unattended-loop sentinel is present |
| observe | All | Captures tool calls for behavioral pattern observation |

### PostToolUse

| Hook | Trigger | Description |
|------|---------|-------------|
| quality-check | Edit on .ts/.tsx/.js/.jsx | Auto-format (Prettier), type-check (TSC), console.log warnings |
| observe | All | Captures tool call results for behavioral pattern observation |
| compact-suggester | All | Counts tool calls, suggests /compact at 50, then every 25 |
| arc-remind | Bash, Edit, Write | Contextual nudges: PR boundary after green tests, eval-before-ship on skill edits, spec planning, main-branch warning |

### Stop

| Hook | Trigger | Description |
|------|---------|-------------|
| session-tracker/end | All | Saves session metrics (JSON + Markdown summary) |

### PreCompact

| Hook | Trigger | Description |
|------|---------|-------------|
| pre-compact | All | Logs compaction event, marks session file with compaction timestamp |

## Adding New Hooks

1. Create a folder named after the hook's purpose (e.g., `my-hook/`)
2. Add `main.js` as the entry point (Node.js for cross-platform support)
3. Add `README.md` documenting the hook
4. Register in `hooks.json`

### Hook Template

```javascript
#!/usr/bin/env node
const { readStdinSync, log } = require('../../scripts/lib/utils');

function main() {
  // Read and pass through stdin (for hook chaining)
  const stdin = readStdinSync();
  process.stdout.write(stdin);

  // Your logic here
  // log() goes to stderr — internal diagnostics only (invisible to users)
  log('[my-hook] Something happened');
}

main();
```

### hooks.json Entry

```json
{
  "PostToolUse": [
    {
      "matcher": "Edit",
      "hooks": [
        {
          "type": "command",
          "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/my-hook/main.js\""
        }
      ]
    }
  ]
}
```

## Available Hook Events

| Event | Trigger | Common Use Cases |
|-------|---------|------------------|
| SessionStart | startup, resume, clear, compact | Context injection, counter reset |
| PreToolUse | Before tool execution | Block dangerous operations, suggest alternatives |
| PostToolUse | After tool completion | Auto-format, type-check, count tools |
| UserPromptSubmit | When user submits prompt | Input validation, context injection |
| Notification | On notification events | Custom notifications |
| PreCompact | Before context compaction | State marking, checkpoint creation |
| Stop | When Claude stops | Save state, cleanup, summaries |
| SubagentStop | When subagent stops | Subagent-specific cleanup |

## Shared Utilities

Hooks import from `scripts/lib/` (canonical source). Key functions:

### scripts/lib/utils.js

- `escapeForJson(str)` - Safe JSON string escaping
- `fileExists(path)` - Check file existence
- `readFileSafe(path, default)` - Read file, returns default on error
- `writeFileSafe(path, content)` - Write file with directory creation
- `readStdinSync()` - Read all stdin content
- `log(msg)` - Log to stderr (internal diagnostics only — invisible to users)
- `output(obj)` - Write JSON to stdout (`{ systemMessage }` for user-visible output)
- `outputContext(context, eventName)` - Output structured hook response for Claude

### scripts/lib/package-manager.js

- `detectPackageManager(dir)` - Detect npm/pnpm/yarn/bun from lock files
- `getPmExecCommand(binary, pm)` - Get exec command for a binary
- `hasDevDependency(pkg, dir)` - Check if package is in devDependencies
- `hasScript(name, dir)` - Check if npm script exists

## Cross-Platform Notes

- Use Node.js instead of bash for Windows compatibility
- Use `execCommand` (which uses `execFileSync`) to prevent shell injection
- Use `path.join()` for file paths
- Temp files go to `os.tmpdir()` (via `getTempDir()`)
