aiwcli 0.12.3 → 0.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dev.cmd +3 -3
- package/bin/dev.js +16 -16
- package/bin/run.cmd +3 -3
- package/bin/run.js +21 -21
- package/dist/commands/branch.js +7 -2
- package/dist/lib/bmad-installer.js +37 -37
- package/dist/lib/terminal.d.ts +2 -0
- package/dist/lib/terminal.js +57 -7
- package/dist/templates/CLAUDE.md +205 -205
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +12 -64
- package/dist/templates/_shared/.claude/commands/handoff.md +12 -198
- package/dist/templates/_shared/.claude/settings.json +65 -65
- package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
- package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -0
- package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/document-generator.ts +215 -216
- package/dist/templates/_shared/{lib-ts/handoff → handoff-system/lib}/handoff-reader.ts +157 -158
- package/dist/templates/_shared/{scripts → handoff-system/scripts}/resume_handoff.ts +373 -373
- package/dist/templates/_shared/{scripts → handoff-system/scripts}/save_handoff.ts +469 -358
- package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -0
- package/dist/templates/_shared/{workflows → handoff-system/workflows}/handoff.md +254 -254
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
- package/dist/templates/_shared/hooks-ts/session_end.ts +196 -183
- package/dist/templates/_shared/hooks-ts/session_start.ts +163 -151
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
- package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
- package/dist/templates/_shared/lib-ts/base/constants.ts +303 -303
- package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
- package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
- package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
- package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -130
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +56 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -560
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -515
- package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -668
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
- package/dist/templates/_shared/lib-ts/package.json +20 -20
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
- package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
- package/dist/templates/_shared/lib-ts/types.ts +186 -180
- package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
- package/dist/templates/_shared/scripts/status_line.ts +690 -690
- package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/ask.md +136 -136
- package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/index.md +21 -21
- package/dist/templates/cc-native/.claude/commands/{rlm → cc-native/rlm}/overview.md +56 -56
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
- package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
- package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
- package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
- package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
- package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
- package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
- package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
- package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -65
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -195
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -447
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
- package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
- package/oclif.manifest.json +1 -1
- package/package.json +108 -108
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
|
@@ -1,367 +1,367 @@
|
|
|
1
|
-
# Shared TypeScript Library
|
|
2
|
-
|
|
3
|
-
**Location:** `_shared/lib-ts/` — cross-method infrastructure used by ALL templates.
|
|
4
|
-
|
|
5
|
-
**One import gets you started:**
|
|
6
|
-
```typescript
|
|
7
|
-
import { loadHookInput, runHook, logInfo, emitContext } from "../lib-ts/base/hook-utils.js";
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
`hook-utils.ts` re-exports the most-used functions from `logger.ts`, `constants.ts`, and `context-store.ts`. Start here. Only import from deeper modules when you need specific capabilities.
|
|
11
|
-
|
|
12
|
-
**Import direction:** Hooks --> method lib --> `_shared/lib-ts/`. Never the reverse.
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## Critical Rules
|
|
17
|
-
|
|
18
|
-
These cause silent failures or UI noise when violated:
|
|
19
|
-
|
|
20
|
-
- **Entry point:** Every hook MUST use `runHook()` or `runHookAsync()` — never bare `main()` or `process.exit()`
|
|
21
|
-
- **stdout is sacred:** Only hook JSON output goes to stdout. Use logger functions for diagnostics, never `console.log()` or `print()`
|
|
22
|
-
- **stderr is opt-in:** `logDebug/logInfo/logWarn/logError` write to file only. Use `logBlocking()` when you NEED stderr visibility
|
|
23
|
-
- **Catch non-critical errors locally:** Uncaught errors bubble to `runHook` which writes to stderr, showing "hook error" in the UI even on exit 0
|
|
24
|
-
- **No reverse imports:** Never import from method lib (e.g., `_cc-native/lib/`) into shared lib
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Hook Skeleton
|
|
29
|
-
|
|
30
|
-
Copy this for new hooks:
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
#!/usr/bin/env bun
|
|
34
|
-
import { loadHookInput, runHook, logDebug, logInfo, emitContext } from "../lib-ts/base/hook-utils.js";
|
|
35
|
-
|
|
36
|
-
function main(): void {
|
|
37
|
-
const payload = loadHookInput();
|
|
38
|
-
if (!payload) return;
|
|
39
|
-
|
|
40
|
-
const sessionId = payload.session_id;
|
|
41
|
-
if (!sessionId) return;
|
|
42
|
-
|
|
43
|
-
// Your hook logic here...
|
|
44
|
-
|
|
45
|
-
emitContext("Context visible to Claude");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
runHook(main, "my_hook_name");
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
For async hooks (AI inference, network calls):
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
import { runHookAsync } from "../lib-ts/base/hook-utils.js";
|
|
55
|
-
|
|
56
|
-
async function asyncMain(): Promise<void> {
|
|
57
|
-
// await something...
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
runHookAsync(asyncMain, "my_async_hook");
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## Logging
|
|
66
|
-
|
|
67
|
-
All logging goes to `_output/hook-log.jsonl`. stderr visibility is opt-in.
|
|
68
|
-
|
|
69
|
-
| Tier | Function | Visible in UI? | Use When |
|
|
70
|
-
|------|----------|---------------|----------|
|
|
71
|
-
| File-only | `logDebug()` / `logInfo()` / `logWarn()` / `logError()` | No | 99% of logging: diagnostics, state changes, non-critical errors |
|
|
72
|
-
| Blocking | `logBlocking()` | Yes (stderr) | The hook found a real problem the user or Claude must see |
|
|
73
|
-
| Unhandled | `logHookError()` | Yes (stderr) | Reserved for `runHook` crash handler — do not call directly |
|
|
74
|
-
| Terminal | `eprint()` | Yes (raw stderr) | Usage help, progress indicators — not logged to JSONL |
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
import { logDebug, logInfo, logWarn, logBlocking } from "../lib-ts/base/hook-utils.js";
|
|
78
|
-
|
|
79
|
-
logInfo("my_hook", "Session started"); // file only
|
|
80
|
-
logWarn("my_hook", `Fallback used: ${reason}`); // file only
|
|
81
|
-
logBlocking("my_hook", "Critical: state corrupt"); // shows in UI
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
## Hook Output — Communication Channels
|
|
87
|
-
|
|
88
|
-
Hooks have multiple channels back to the session. Pick the right one:
|
|
89
|
-
|
|
90
|
-
| Want to... | Function | Who sees it |
|
|
91
|
-
|------------|----------|-------------|
|
|
92
|
-
| **Block (any event)** | `emitBlock(reason, context?)` | Claude + user — auto-dispatches to correct mechanism |
|
|
93
|
-
| Block tool (PreToolUse only) | `emitContextAndBlock(context, reason)` | Claude + user (denial reason prominent) |
|
|
94
|
-
| Return message, don't block | `emitContext(context)` | Claude + user (in transcript) |
|
|
95
|
-
| Log only (diagnostics) | `logInfo()` / `logWarn()` / etc. | Nobody in session — file only |
|
|
96
|
-
|
|
97
|
-
**There is no way to show something to the user but hide it from Claude, or vice versa.** Both `emitContext()` and `emitContextAndBlock()` produce output visible to both.
|
|
98
|
-
|
|
99
|
-
### Universal Blocking: `emitBlock()` (RECOMMENDED)
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
emitBlock("Reason for blocking", "Optional detailed context");
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Auto-detects the correct blocking mechanism from the current hook event:
|
|
106
|
-
|
|
107
|
-
| Event | Mechanism | Dispatches to |
|
|
108
|
-
|-------|-----------|---------------|
|
|
109
|
-
| **PreToolUse** | `permissionDecision: "deny"` in hookSpecificOutput | `emitContextAndBlock()` |
|
|
110
|
-
| **UserPromptSubmit** | Top-level `{ decision: "block", reason }` | `emitBlockPrompt()` |
|
|
111
|
-
| **PostToolUse / PostToolUseFailure** | Exit code 2 + stderr | `emitBlockViaExit()` |
|
|
112
|
-
| **Stop / SubagentStop** | Top-level `{ decision: "block", reason }` | `emitBlockTopLevel()` |
|
|
113
|
-
| **PermissionRequest** | `{ decision: { behavior: "deny" } }` | `emitPermissionDecision()` |
|
|
114
|
-
| **SessionStart / Notification / SubagentStart / SessionEnd** | No blocking mechanism — warns and no-ops | — |
|
|
115
|
-
|
|
116
|
-
**Use `emitBlock()` for all new hooks.** The per-pattern functions below exist for advanced use cases only.
|
|
117
|
-
|
|
118
|
-
### Channel 1: Block + Context (PreToolUse only)
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
emitContextAndBlock(
|
|
122
|
-
"Detailed feedback Claude sees", // additionalContext
|
|
123
|
-
"Short reason for the block" // permissionDecisionReason
|
|
124
|
-
);
|
|
125
|
-
// No SystemExit needed — permissionDecision:"deny" with exit 0 is sufficient.
|
|
126
|
-
// Warns if called from non-PreToolUse events (output will be silently rejected by Claude Code).
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
The tool call is **prevented from executing**. Only works for PreToolUse hooks.
|
|
130
|
-
|
|
131
|
-
### Channel 2: Block Prompt Submission (UserPromptSubmit only)
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
emitBlockPrompt("Reason the prompt was blocked", "Optional context for Claude");
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Emits top-level `{ decision: "block", reason }`. The prompt is rejected before Claude processes it.
|
|
138
|
-
|
|
139
|
-
### Channel 3: Block via Exit Code (PostToolUse / PostToolUseFailure)
|
|
140
|
-
|
|
141
|
-
```typescript
|
|
142
|
-
emitBlockViaExit("Reason for blocking", "Optional context prepended to stderr");
|
|
143
|
-
// Throws SystemExit:2 — handled by runHook/runHookAsync
|
|
144
|
-
// NOTE: Exit 2 causes Claude Code to ignore all JSON stdout — only stderr matters
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Channel 4: Block Stop/SubagentStop
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
emitBlockTopLevel("Reason to prevent stopping");
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### Channel 5: PermissionRequest Decision
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
emitPermissionDecision("deny", { message: "Why denied" });
|
|
157
|
-
emitPermissionDecision("allow", { updatedInput: { /* modified input */ } });
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### Channel 6: Non-blocking Context (any hook event)
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
emitContext("Information added to Claude's context");
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
The tool call / session continues normally. Works for PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Notification, SubagentStart.
|
|
167
|
-
|
|
168
|
-
### Channel 7: Log-only (diagnostics)
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
logInfo("my_hook", "Processing started"); // File only
|
|
172
|
-
logWarn("my_hook", `Fallback used: ${why}`); // File only
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Nobody in the session sees this. Written to `_output/hook-log.jsonl` for debugging.
|
|
176
|
-
|
|
177
|
-
### Hook Output Logging
|
|
178
|
-
|
|
179
|
-
Both `emitContext()` and `emitContextAndBlock()` automatically log their output to `_output/hook-log.jsonl` as `HOOK_OUTPUT` entries. This captures exactly what was sent to Claude via stdout, closing the visibility gap where the agent sees injected context the user doesn't.
|
|
180
|
-
|
|
181
|
-
- **Log level:** `info` — visible unless `HOOK_LOG_LEVEL=warn`
|
|
182
|
-
- **`msg` field:** Scannable summary: `HOOK_OUTPUT [context] 842 chars` or `HOOK_OUTPUT [block] 340 chars, reason="..."`
|
|
183
|
-
- **`data` field:** Full payload including `additionalContext` and `blockReason` (for blocks)
|
|
184
|
-
- **Controlled by:** Existing `HOOK_LOG_LEVEL` and `HOOK_LOG_DISABLE` env vars
|
|
185
|
-
- **No hook changes needed:** Logging happens inside the emit functions themselves
|
|
186
|
-
|
|
187
|
-
### Exit codes and JSON
|
|
188
|
-
|
|
189
|
-
| Exit Code | JSON Parsed? | Effect |
|
|
190
|
-
|-----------|-------------|--------|
|
|
191
|
-
| **0** | Yes | Normal — `hookSpecificOutput` processed |
|
|
192
|
-
| **2** | No | Blocking error — JSON ignored, stderr fed to Claude |
|
|
193
|
-
| **Other** | No | Non-blocking error — stderr shown in verbose mode |
|
|
194
|
-
|
|
195
|
-
You cannot mix exit 2 with JSON decisions. Pick one: exit 0 + JSON, or exit 2 + stderr.
|
|
196
|
-
|
|
197
|
-
### hookSpecificOutput fields by event type
|
|
198
|
-
|
|
199
|
-
| Event | `additionalContext` | `permissionDecision` | `permissionDecisionReason` | Other |
|
|
200
|
-
|-------|:--:|:--:|:--:|-------|
|
|
201
|
-
| **PreToolUse** | Y | Y (allow/deny/ask) | Y | `updatedInput` |
|
|
202
|
-
| **PostToolUse** | Y | - | - | `updatedMCPToolOutput` (MCP only) |
|
|
203
|
-
| **UserPromptSubmit** | Y | - | - | top-level `decision: "block"` |
|
|
204
|
-
| **SessionStart** | Y | - | - | — |
|
|
205
|
-
| **Notification** | Y | - | - | — |
|
|
206
|
-
| **SubagentStart** | Y | - | - | — |
|
|
207
|
-
| **Stop** | - | - | - | top-level `decision`, `reason` |
|
|
208
|
-
| **SessionEnd** | - | - | - | — |
|
|
209
|
-
|
|
210
|
-
**Invalid fields cause silent rejection of the entire output.** No error, no feedback. Conversely, **missing `hookEventName` also causes silent rejection** — see "Hook API: Critical Learnings" below.
|
|
211
|
-
|
|
212
|
-
### Special case: fileSuggestion
|
|
213
|
-
|
|
214
|
-
The `fileSuggestion` settings command is NOT a hook — it uses a different protocol. It outputs a plain JSON array to stdout (e.g., `console.log(JSON.stringify(paths))`). Do not use `emitContext()` for fileSuggestion.
|
|
215
|
-
|
|
216
|
-
---
|
|
217
|
-
|
|
218
|
-
## Hook API: Critical Learnings (Verified 2026-02-11)
|
|
219
|
-
|
|
220
|
-
These findings were verified through systematic testing. They document Claude Code's actual behavior, which sometimes differs from what the docs suggest.
|
|
221
|
-
|
|
222
|
-
### hookEventName is REQUIRED (CC 2.1.39+)
|
|
223
|
-
|
|
224
|
-
Claude Code validates `hookSpecificOutput` using a Zod discriminated union keyed on `hookEventName`. If this field is missing:
|
|
225
|
-
|
|
226
|
-
- The entire hook output is silently rejected — no error, no feedback
|
|
227
|
-
- `permissionDecision: "deny"` is never processed
|
|
228
|
-
- The hook appears to "not work" even though it runs successfully
|
|
229
|
-
|
|
230
|
-
**You don't need to handle this manually.** `emitContext()` and `emitContextAndBlock()` auto-detect `hookEventName` from the stdin payload (via `_lastHookEvent`, set by `loadHookInput()`/`runHook()`). This works because hooks are synchronous single-process executions — each `bun` process has its own memory, so there's no concurrency risk between sessions.
|
|
231
|
-
|
|
232
|
-
**If auto-detection fails** (e.g., `loadHookInput()` wasn't called), `hookEventName` is omitted and the output will be silently rejected. This is why `runHook()`/`runHookAsync()` is mandatory — it calls `_earlyReadInput()` first, guaranteeing `_lastHookEvent` is populated.
|
|
233
|
-
|
|
234
|
-
### Exit Code Behavior (Tested)
|
|
235
|
-
|
|
236
|
-
| Exit Code | JSON Parsed? | Blocks Tool? | What Claude Sees | Tested? |
|
|
237
|
-
|-----------|-------------|-------------|------------------|---------|
|
|
238
|
-
| **0** + deny JSON | Yes | Yes (PreToolUse only) | `additionalContext` + denial reason | Yes |
|
|
239
|
-
| **0** + context JSON | Yes | No | `additionalContext` in transcript | Yes |
|
|
240
|
-
| **1** | No | No | stderr in verbose mode only | Yes |
|
|
241
|
-
| **2** | No | Yes (any event) | stderr fed as system-reminder | Yes |
|
|
242
|
-
|
|
243
|
-
**Key insight:** Exit 0 + `permissionDecision: "deny"` is the correct way to block a tool. Exit 2 is a blunt instrument — it ignores your JSON and feeds raw stderr to Claude. Use exit 0 + deny for clean blocking with structured feedback.
|
|
244
|
-
|
|
245
|
-
### ExitPlanMode: Not Special-Cased
|
|
246
|
-
|
|
247
|
-
Early testing suggested ExitPlanMode was "immune" to PreToolUse deny. **This was wrong.** The actual issue was missing `hookEventName` — the Zod validator silently rejected the deny output.
|
|
248
|
-
|
|
249
|
-
**With `hookEventName` included:**
|
|
250
|
-
- PreToolUse `permissionDecision: "deny"` (exit 0) → **blocks ExitPlanMode**, no dialog appears, session stays in plan mode
|
|
251
|
-
- `emitContextAndBlock()` handles this automatically via auto-detection
|
|
252
|
-
|
|
253
|
-
**Without `hookEventName` (the bug):**
|
|
254
|
-
- Deny silently rejected → dialog appeared → looked like ExitPlanMode was special-cased
|
|
255
|
-
- Exit 2 also appeared to "not work" for PreToolUse (JSON was ignored as expected, but the blocking was via stderr, not deny)
|
|
256
|
-
- PostToolUse with exit 2 appeared to work because it used stderr (not JSON), bypassing the Zod issue
|
|
257
|
-
|
|
258
|
-
**Lesson:** When a hook output seems to be "silently ignored," check the JSON schema first. The Zod validator rejects malformed output without any error message.
|
|
259
|
-
|
|
260
|
-
### Debugging Checklist
|
|
261
|
-
|
|
262
|
-
When a hook's deny/context isn't working:
|
|
263
|
-
|
|
264
|
-
1. **Is `hookEventName` in the JSON output?** Check `_output/hook-log.jsonl` for `HOOK_OUTPUT` entries
|
|
265
|
-
2. **Is the hook using `runHook()`/`runHookAsync()`?** Required for auto-detection
|
|
266
|
-
3. **Is `loadHookInput()` called before `emitContext()`?** It populates `_lastHookEvent`
|
|
267
|
-
4. **Is the exit code 0?** Exit 1/2 cause JSON to be ignored
|
|
268
|
-
5. **Are there extra fields in `hookSpecificOutput`?** Invalid fields cause silent rejection of the entire output
|
|
269
|
-
|
|
270
|
-
---
|
|
271
|
-
|
|
272
|
-
## Context Store
|
|
273
|
-
|
|
274
|
-
2-layer CRUD: per-context `state.json` + global `_output/index.json`.
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
import { getContextBySessionId, bindSession, maybeActivate, saveState } from "../lib-ts/context/context-store.js";
|
|
278
|
-
|
|
279
|
-
const state = getContextBySessionId(sessionId, projectRoot);
|
|
280
|
-
if (state) {
|
|
281
|
-
// ALWAYS wrap non-critical operations — uncaught errors become UI "hook error"
|
|
282
|
-
try {
|
|
283
|
-
maybeActivate(state.id, permissionMode, projectRoot, "hook_name");
|
|
284
|
-
} catch (e) {
|
|
285
|
-
logWarn("hook_name", `maybeActivate failed (non-critical): ${e}`);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
**Valid modes:** `idle` | `has_plan` | `has_handoff` | `active`
|
|
291
|
-
|
|
292
|
-
Transitions: `idle`/`has_plan`/`has_handoff` --> `active` (via `maybeActivate`). `active` --> `has_plan`/`has_handoff` (via `session_end`).
|
|
293
|
-
|
|
294
|
-
---
|
|
295
|
-
|
|
296
|
-
## Module Reference
|
|
297
|
-
|
|
298
|
-
Use this table to find the right file. Read the source for full API details.
|
|
299
|
-
|
|
300
|
-
### `base/` — Core Infrastructure
|
|
301
|
-
|
|
302
|
-
| File | Purpose | Key Exports |
|
|
303
|
-
|------|---------|-------------|
|
|
304
|
-
| `hook-utils.ts` | Hook lifecycle, stdin parsing, output emit, re-exports | `runHook`, `runHookAsync`, `loadHookInput`, `emitContext`, `emitContextAndBlock`, `emitBlock`, `emitBlockPrompt`, `emitBlockViaExit`, `emitBlockTopLevel`, `emitPermissionDecision`, `logDebug`...`logBlocking` |
|
|
305
|
-
| `logger.ts` | JSONL logging engine | `hookLog`, `logDebug`, `logInfo`, `logWarn`, `logError`, `logBlocking`, `logHookError`, `logDiagnostic` |
|
|
306
|
-
| `constants.ts` | Path resolution, limits | `getProjectRoot()`, `getContextDir()`, `MAX_FILE_SIZE` |
|
|
307
|
-
| `atomic-write.ts` | Crash-safe file writes | `atomicWriteFileSync()` |
|
|
308
|
-
| `state-io.ts` | State serialization with mode migration | `readState()`, `writeState()` |
|
|
309
|
-
| `inference.ts` | Claude CLI subprocess calls | `inferText()` |
|
|
310
|
-
| `utils.ts` | Formatting, ID generation | `nowIso()`, `generateContextId()`, `slugify()` |
|
|
311
|
-
| `git-state.ts` | Git snapshot | `captureGitState()` |
|
|
312
|
-
| `subprocess-utils.ts` | Recursive call guard | `isInternalCall()` |
|
|
313
|
-
| `stop-words.ts` | Word list for ID generation | Used by `utils.ts` internally |
|
|
314
|
-
|
|
315
|
-
### `context/` — Context State Management
|
|
316
|
-
|
|
317
|
-
| File | Purpose | Key Exports |
|
|
318
|
-
|------|---------|-------------|
|
|
319
|
-
| `context-store.ts` | CRUD for context state + index | `getContextBySessionId`, `bindSession`, `maybeActivate`, `saveState`, `createContext` |
|
|
320
|
-
| `context-selector.ts` | Route prompts to contexts | `determineContext()`, `BlockRequest` |
|
|
321
|
-
| `context-formatter.ts` | Display formatting | `formatContextSummary()` |
|
|
322
|
-
| `plan-manager.ts` | Plan lifecycle (archive, hash, sign) | `archivePlan()`, `computePlanHash()` |
|
|
323
|
-
| `task-tracker.ts` | Task CRUD on state.json | `addTask()`, `updateTask()`, `getTasks()` |
|
|
324
|
-
|
|
325
|
-
### `handoff/` and `templates/`
|
|
326
|
-
|
|
327
|
-
| File | Purpose | Key Exports |
|
|
328
|
-
|------|---------|-------------|
|
|
329
|
-
| `handoff/document-generator.ts` | Handoff document generation | `generateHandoffDocument()` |
|
|
330
|
-
| `templates/formatters.ts` | Display constants, mode maps, icons | `MODE_MAP`, `STATUS_ICONS` |
|
|
331
|
-
| `templates/plan-context.ts` | Plan evaluation templates | `PLAN_EVALUATION_REMINDER` |
|
|
332
|
-
|
|
333
|
-
### Root
|
|
334
|
-
|
|
335
|
-
| File | Purpose |
|
|
336
|
-
|------|---------|
|
|
337
|
-
| `types.ts` | All shared types: `Mode`, `ContextState`, `Task`, `HookInput`, `HookOutput` |
|
|
338
|
-
|
|
339
|
-
---
|
|
340
|
-
|
|
341
|
-
## Shared Hooks (`_shared/hooks-ts/`)
|
|
342
|
-
|
|
343
|
-
These run for ALL templates. Method-specific hooks live in `_{method}/hooks/`.
|
|
344
|
-
|
|
345
|
-
| Hook | Event | Purpose |
|
|
346
|
-
|------|-------|---------|
|
|
347
|
-
| `user_prompt_submit.ts` | UserPromptSubmit | Context enforcement — binds prompts to tracked contexts |
|
|
348
|
-
| `context_monitor.ts` | PostToolUse:* | Context window tracking, handoff warnings at 30/20/10% |
|
|
349
|
-
| `session_start.ts` | SessionStart | Restores plan/handoff context after `/clear` or compaction |
|
|
350
|
-
| `session_end.ts` | SessionEnd | Stages `active` --> `has_plan`/`has_handoff` for next session |
|
|
351
|
-
| `archive_plan.ts` | PreToolUse:ExitPlanMode | Archives plan file before accept/reject decision |
|
|
352
|
-
| `pre_compact.ts` | PreToolUse:Compact | Pre-compaction state snapshot |
|
|
353
|
-
| `task_create_capture.ts` | PostToolUse:TaskCreate | Persists task creation to context state |
|
|
354
|
-
| `task_update_capture.ts` | PostToolUse:TaskUpdate | Persists task updates to context state |
|
|
355
|
-
| `file-suggestion.ts` | PostToolUse:Write | Suggests file organization improvements |
|
|
356
|
-
|
|
357
|
-
---
|
|
358
|
-
|
|
359
|
-
## Environment Variables
|
|
360
|
-
|
|
361
|
-
| Variable | Effect |
|
|
362
|
-
|----------|--------|
|
|
363
|
-
| `CLAUDE_PROJECT_DIR` | Override project root detection |
|
|
364
|
-
| `HOOK_LOG_DISABLE=1` | Disable all file logging |
|
|
365
|
-
| `HOOK_LOG_LEVEL=warn` | Minimum log level (default: `debug`) |
|
|
366
|
-
| `HOOK_ERROR_LOG_DISABLE=1` | Legacy alias for `HOOK_LOG_DISABLE` |
|
|
367
|
-
| `_CC_INTERNAL=1` | Marks subprocess calls (checked by `isInternalCall()`) |
|
|
1
|
+
# Shared TypeScript Library
|
|
2
|
+
|
|
3
|
+
**Location:** `_shared/lib-ts/` — cross-method infrastructure used by ALL templates.
|
|
4
|
+
|
|
5
|
+
**One import gets you started:**
|
|
6
|
+
```typescript
|
|
7
|
+
import { loadHookInput, runHook, logInfo, emitContext } from "../lib-ts/base/hook-utils.js";
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`hook-utils.ts` re-exports the most-used functions from `logger.ts`, `constants.ts`, and `context-store.ts`. Start here. Only import from deeper modules when you need specific capabilities.
|
|
11
|
+
|
|
12
|
+
**Import direction:** Hooks --> method lib --> `_shared/lib-ts/`. Never the reverse.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Critical Rules
|
|
17
|
+
|
|
18
|
+
These cause silent failures or UI noise when violated:
|
|
19
|
+
|
|
20
|
+
- **Entry point:** Every hook MUST use `runHook()` or `runHookAsync()` — never bare `main()` or `process.exit()`
|
|
21
|
+
- **stdout is sacred:** Only hook JSON output goes to stdout. Use logger functions for diagnostics, never `console.log()` or `print()`
|
|
22
|
+
- **stderr is opt-in:** `logDebug/logInfo/logWarn/logError` write to file only. Use `logBlocking()` when you NEED stderr visibility
|
|
23
|
+
- **Catch non-critical errors locally:** Uncaught errors bubble to `runHook` which writes to stderr, showing "hook error" in the UI even on exit 0
|
|
24
|
+
- **No reverse imports:** Never import from method lib (e.g., `_cc-native/lib/`) into shared lib
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Hook Skeleton
|
|
29
|
+
|
|
30
|
+
Copy this for new hooks:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
#!/usr/bin/env bun
|
|
34
|
+
import { loadHookInput, runHook, logDebug, logInfo, emitContext } from "../lib-ts/base/hook-utils.js";
|
|
35
|
+
|
|
36
|
+
function main(): void {
|
|
37
|
+
const payload = loadHookInput();
|
|
38
|
+
if (!payload) return;
|
|
39
|
+
|
|
40
|
+
const sessionId = payload.session_id;
|
|
41
|
+
if (!sessionId) return;
|
|
42
|
+
|
|
43
|
+
// Your hook logic here...
|
|
44
|
+
|
|
45
|
+
emitContext("Context visible to Claude");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
runHook(main, "my_hook_name");
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For async hooks (AI inference, network calls):
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { runHookAsync } from "../lib-ts/base/hook-utils.js";
|
|
55
|
+
|
|
56
|
+
async function asyncMain(): Promise<void> {
|
|
57
|
+
// await something...
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
runHookAsync(asyncMain, "my_async_hook");
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Logging
|
|
66
|
+
|
|
67
|
+
All logging goes to `_output/hook-log.jsonl`. stderr visibility is opt-in.
|
|
68
|
+
|
|
69
|
+
| Tier | Function | Visible in UI? | Use When |
|
|
70
|
+
|------|----------|---------------|----------|
|
|
71
|
+
| File-only | `logDebug()` / `logInfo()` / `logWarn()` / `logError()` | No | 99% of logging: diagnostics, state changes, non-critical errors |
|
|
72
|
+
| Blocking | `logBlocking()` | Yes (stderr) | The hook found a real problem the user or Claude must see |
|
|
73
|
+
| Unhandled | `logHookError()` | Yes (stderr) | Reserved for `runHook` crash handler — do not call directly |
|
|
74
|
+
| Terminal | `eprint()` | Yes (raw stderr) | Usage help, progress indicators — not logged to JSONL |
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { logDebug, logInfo, logWarn, logBlocking } from "../lib-ts/base/hook-utils.js";
|
|
78
|
+
|
|
79
|
+
logInfo("my_hook", "Session started"); // file only
|
|
80
|
+
logWarn("my_hook", `Fallback used: ${reason}`); // file only
|
|
81
|
+
logBlocking("my_hook", "Critical: state corrupt"); // shows in UI
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Hook Output — Communication Channels
|
|
87
|
+
|
|
88
|
+
Hooks have multiple channels back to the session. Pick the right one:
|
|
89
|
+
|
|
90
|
+
| Want to... | Function | Who sees it |
|
|
91
|
+
|------------|----------|-------------|
|
|
92
|
+
| **Block (any event)** | `emitBlock(reason, context?)` | Claude + user — auto-dispatches to correct mechanism |
|
|
93
|
+
| Block tool (PreToolUse only) | `emitContextAndBlock(context, reason)` | Claude + user (denial reason prominent) |
|
|
94
|
+
| Return message, don't block | `emitContext(context)` | Claude + user (in transcript) |
|
|
95
|
+
| Log only (diagnostics) | `logInfo()` / `logWarn()` / etc. | Nobody in session — file only |
|
|
96
|
+
|
|
97
|
+
**There is no way to show something to the user but hide it from Claude, or vice versa.** Both `emitContext()` and `emitContextAndBlock()` produce output visible to both.
|
|
98
|
+
|
|
99
|
+
### Universal Blocking: `emitBlock()` (RECOMMENDED)
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
emitBlock("Reason for blocking", "Optional detailed context");
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Auto-detects the correct blocking mechanism from the current hook event:
|
|
106
|
+
|
|
107
|
+
| Event | Mechanism | Dispatches to |
|
|
108
|
+
|-------|-----------|---------------|
|
|
109
|
+
| **PreToolUse** | `permissionDecision: "deny"` in hookSpecificOutput | `emitContextAndBlock()` |
|
|
110
|
+
| **UserPromptSubmit** | Top-level `{ decision: "block", reason }` | `emitBlockPrompt()` |
|
|
111
|
+
| **PostToolUse / PostToolUseFailure** | Exit code 2 + stderr | `emitBlockViaExit()` |
|
|
112
|
+
| **Stop / SubagentStop** | Top-level `{ decision: "block", reason }` | `emitBlockTopLevel()` |
|
|
113
|
+
| **PermissionRequest** | `{ decision: { behavior: "deny" } }` | `emitPermissionDecision()` |
|
|
114
|
+
| **SessionStart / Notification / SubagentStart / SessionEnd** | No blocking mechanism — warns and no-ops | — |
|
|
115
|
+
|
|
116
|
+
**Use `emitBlock()` for all new hooks.** The per-pattern functions below exist for advanced use cases only.
|
|
117
|
+
|
|
118
|
+
### Channel 1: Block + Context (PreToolUse only)
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
emitContextAndBlock(
|
|
122
|
+
"Detailed feedback Claude sees", // additionalContext
|
|
123
|
+
"Short reason for the block" // permissionDecisionReason
|
|
124
|
+
);
|
|
125
|
+
// No SystemExit needed — permissionDecision:"deny" with exit 0 is sufficient.
|
|
126
|
+
// Warns if called from non-PreToolUse events (output will be silently rejected by Claude Code).
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The tool call is **prevented from executing**. Only works for PreToolUse hooks.
|
|
130
|
+
|
|
131
|
+
### Channel 2: Block Prompt Submission (UserPromptSubmit only)
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
emitBlockPrompt("Reason the prompt was blocked", "Optional context for Claude");
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Emits top-level `{ decision: "block", reason }`. The prompt is rejected before Claude processes it.
|
|
138
|
+
|
|
139
|
+
### Channel 3: Block via Exit Code (PostToolUse / PostToolUseFailure)
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
emitBlockViaExit("Reason for blocking", "Optional context prepended to stderr");
|
|
143
|
+
// Throws SystemExit:2 — handled by runHook/runHookAsync
|
|
144
|
+
// NOTE: Exit 2 causes Claude Code to ignore all JSON stdout — only stderr matters
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Channel 4: Block Stop/SubagentStop
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
emitBlockTopLevel("Reason to prevent stopping");
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Channel 5: PermissionRequest Decision
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
emitPermissionDecision("deny", { message: "Why denied" });
|
|
157
|
+
emitPermissionDecision("allow", { updatedInput: { /* modified input */ } });
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Channel 6: Non-blocking Context (any hook event)
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
emitContext("Information added to Claude's context");
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The tool call / session continues normally. Works for PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Notification, SubagentStart.
|
|
167
|
+
|
|
168
|
+
### Channel 7: Log-only (diagnostics)
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
logInfo("my_hook", "Processing started"); // File only
|
|
172
|
+
logWarn("my_hook", `Fallback used: ${why}`); // File only
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Nobody in the session sees this. Written to `_output/hook-log.jsonl` for debugging.
|
|
176
|
+
|
|
177
|
+
### Hook Output Logging
|
|
178
|
+
|
|
179
|
+
Both `emitContext()` and `emitContextAndBlock()` automatically log their output to `_output/hook-log.jsonl` as `HOOK_OUTPUT` entries. This captures exactly what was sent to Claude via stdout, closing the visibility gap where the agent sees injected context the user doesn't.
|
|
180
|
+
|
|
181
|
+
- **Log level:** `info` — visible unless `HOOK_LOG_LEVEL=warn`
|
|
182
|
+
- **`msg` field:** Scannable summary: `HOOK_OUTPUT [context] 842 chars` or `HOOK_OUTPUT [block] 340 chars, reason="..."`
|
|
183
|
+
- **`data` field:** Full payload including `additionalContext` and `blockReason` (for blocks)
|
|
184
|
+
- **Controlled by:** Existing `HOOK_LOG_LEVEL` and `HOOK_LOG_DISABLE` env vars
|
|
185
|
+
- **No hook changes needed:** Logging happens inside the emit functions themselves
|
|
186
|
+
|
|
187
|
+
### Exit codes and JSON
|
|
188
|
+
|
|
189
|
+
| Exit Code | JSON Parsed? | Effect |
|
|
190
|
+
|-----------|-------------|--------|
|
|
191
|
+
| **0** | Yes | Normal — `hookSpecificOutput` processed |
|
|
192
|
+
| **2** | No | Blocking error — JSON ignored, stderr fed to Claude |
|
|
193
|
+
| **Other** | No | Non-blocking error — stderr shown in verbose mode |
|
|
194
|
+
|
|
195
|
+
You cannot mix exit 2 with JSON decisions. Pick one: exit 0 + JSON, or exit 2 + stderr.
|
|
196
|
+
|
|
197
|
+
### hookSpecificOutput fields by event type
|
|
198
|
+
|
|
199
|
+
| Event | `additionalContext` | `permissionDecision` | `permissionDecisionReason` | Other |
|
|
200
|
+
|-------|:--:|:--:|:--:|-------|
|
|
201
|
+
| **PreToolUse** | Y | Y (allow/deny/ask) | Y | `updatedInput` |
|
|
202
|
+
| **PostToolUse** | Y | - | - | `updatedMCPToolOutput` (MCP only) |
|
|
203
|
+
| **UserPromptSubmit** | Y | - | - | top-level `decision: "block"` |
|
|
204
|
+
| **SessionStart** | Y | - | - | — |
|
|
205
|
+
| **Notification** | Y | - | - | — |
|
|
206
|
+
| **SubagentStart** | Y | - | - | — |
|
|
207
|
+
| **Stop** | - | - | - | top-level `decision`, `reason` |
|
|
208
|
+
| **SessionEnd** | - | - | - | — |
|
|
209
|
+
|
|
210
|
+
**Invalid fields cause silent rejection of the entire output.** No error, no feedback. Conversely, **missing `hookEventName` also causes silent rejection** — see "Hook API: Critical Learnings" below.
|
|
211
|
+
|
|
212
|
+
### Special case: fileSuggestion
|
|
213
|
+
|
|
214
|
+
The `fileSuggestion` settings command is NOT a hook — it uses a different protocol. It outputs a plain JSON array to stdout (e.g., `console.log(JSON.stringify(paths))`). Do not use `emitContext()` for fileSuggestion.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Hook API: Critical Learnings (Verified 2026-02-11)
|
|
219
|
+
|
|
220
|
+
These findings were verified through systematic testing. They document Claude Code's actual behavior, which sometimes differs from what the docs suggest.
|
|
221
|
+
|
|
222
|
+
### hookEventName is REQUIRED (CC 2.1.39+)
|
|
223
|
+
|
|
224
|
+
Claude Code validates `hookSpecificOutput` using a Zod discriminated union keyed on `hookEventName`. If this field is missing:
|
|
225
|
+
|
|
226
|
+
- The entire hook output is silently rejected — no error, no feedback
|
|
227
|
+
- `permissionDecision: "deny"` is never processed
|
|
228
|
+
- The hook appears to "not work" even though it runs successfully
|
|
229
|
+
|
|
230
|
+
**You don't need to handle this manually.** `emitContext()` and `emitContextAndBlock()` auto-detect `hookEventName` from the stdin payload (via `_lastHookEvent`, set by `loadHookInput()`/`runHook()`). This works because hooks are synchronous single-process executions — each `bun` process has its own memory, so there's no concurrency risk between sessions.
|
|
231
|
+
|
|
232
|
+
**If auto-detection fails** (e.g., `loadHookInput()` wasn't called), `hookEventName` is omitted and the output will be silently rejected. This is why `runHook()`/`runHookAsync()` is mandatory — it calls `_earlyReadInput()` first, guaranteeing `_lastHookEvent` is populated.
|
|
233
|
+
|
|
234
|
+
### Exit Code Behavior (Tested)
|
|
235
|
+
|
|
236
|
+
| Exit Code | JSON Parsed? | Blocks Tool? | What Claude Sees | Tested? |
|
|
237
|
+
|-----------|-------------|-------------|------------------|---------|
|
|
238
|
+
| **0** + deny JSON | Yes | Yes (PreToolUse only) | `additionalContext` + denial reason | Yes |
|
|
239
|
+
| **0** + context JSON | Yes | No | `additionalContext` in transcript | Yes |
|
|
240
|
+
| **1** | No | No | stderr in verbose mode only | Yes |
|
|
241
|
+
| **2** | No | Yes (any event) | stderr fed as system-reminder | Yes |
|
|
242
|
+
|
|
243
|
+
**Key insight:** Exit 0 + `permissionDecision: "deny"` is the correct way to block a tool. Exit 2 is a blunt instrument — it ignores your JSON and feeds raw stderr to Claude. Use exit 0 + deny for clean blocking with structured feedback.
|
|
244
|
+
|
|
245
|
+
### ExitPlanMode: Not Special-Cased
|
|
246
|
+
|
|
247
|
+
Early testing suggested ExitPlanMode was "immune" to PreToolUse deny. **This was wrong.** The actual issue was missing `hookEventName` — the Zod validator silently rejected the deny output.
|
|
248
|
+
|
|
249
|
+
**With `hookEventName` included:**
|
|
250
|
+
- PreToolUse `permissionDecision: "deny"` (exit 0) → **blocks ExitPlanMode**, no dialog appears, session stays in plan mode
|
|
251
|
+
- `emitContextAndBlock()` handles this automatically via auto-detection
|
|
252
|
+
|
|
253
|
+
**Without `hookEventName` (the bug):**
|
|
254
|
+
- Deny silently rejected → dialog appeared → looked like ExitPlanMode was special-cased
|
|
255
|
+
- Exit 2 also appeared to "not work" for PreToolUse (JSON was ignored as expected, but the blocking was via stderr, not deny)
|
|
256
|
+
- PostToolUse with exit 2 appeared to work because it used stderr (not JSON), bypassing the Zod issue
|
|
257
|
+
|
|
258
|
+
**Lesson:** When a hook output seems to be "silently ignored," check the JSON schema first. The Zod validator rejects malformed output without any error message.
|
|
259
|
+
|
|
260
|
+
### Debugging Checklist
|
|
261
|
+
|
|
262
|
+
When a hook's deny/context isn't working:
|
|
263
|
+
|
|
264
|
+
1. **Is `hookEventName` in the JSON output?** Check `_output/hook-log.jsonl` for `HOOK_OUTPUT` entries
|
|
265
|
+
2. **Is the hook using `runHook()`/`runHookAsync()`?** Required for auto-detection
|
|
266
|
+
3. **Is `loadHookInput()` called before `emitContext()`?** It populates `_lastHookEvent`
|
|
267
|
+
4. **Is the exit code 0?** Exit 1/2 cause JSON to be ignored
|
|
268
|
+
5. **Are there extra fields in `hookSpecificOutput`?** Invalid fields cause silent rejection of the entire output
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Context Store
|
|
273
|
+
|
|
274
|
+
2-layer CRUD: per-context `state.json` + global `_output/index.json`.
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { getContextBySessionId, bindSession, maybeActivate, saveState } from "../lib-ts/context/context-store.js";
|
|
278
|
+
|
|
279
|
+
const state = getContextBySessionId(sessionId, projectRoot);
|
|
280
|
+
if (state) {
|
|
281
|
+
// ALWAYS wrap non-critical operations — uncaught errors become UI "hook error"
|
|
282
|
+
try {
|
|
283
|
+
maybeActivate(state.id, permissionMode, projectRoot, "hook_name");
|
|
284
|
+
} catch (e) {
|
|
285
|
+
logWarn("hook_name", `maybeActivate failed (non-critical): ${e}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Valid modes:** `idle` | `has_plan` | `has_handoff` | `active`
|
|
291
|
+
|
|
292
|
+
Transitions: `idle`/`has_plan`/`has_handoff` --> `active` (via `maybeActivate`). `active` --> `has_plan`/`has_handoff` (via `session_end`).
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Module Reference
|
|
297
|
+
|
|
298
|
+
Use this table to find the right file. Read the source for full API details.
|
|
299
|
+
|
|
300
|
+
### `base/` — Core Infrastructure
|
|
301
|
+
|
|
302
|
+
| File | Purpose | Key Exports |
|
|
303
|
+
|------|---------|-------------|
|
|
304
|
+
| `hook-utils.ts` | Hook lifecycle, stdin parsing, output emit, re-exports | `runHook`, `runHookAsync`, `loadHookInput`, `emitContext`, `emitContextAndBlock`, `emitBlock`, `emitBlockPrompt`, `emitBlockViaExit`, `emitBlockTopLevel`, `emitPermissionDecision`, `logDebug`...`logBlocking` |
|
|
305
|
+
| `logger.ts` | JSONL logging engine | `hookLog`, `logDebug`, `logInfo`, `logWarn`, `logError`, `logBlocking`, `logHookError`, `logDiagnostic` |
|
|
306
|
+
| `constants.ts` | Path resolution, limits | `getProjectRoot()`, `getContextDir()`, `MAX_FILE_SIZE` |
|
|
307
|
+
| `atomic-write.ts` | Crash-safe file writes | `atomicWriteFileSync()` |
|
|
308
|
+
| `state-io.ts` | State serialization with mode migration | `readState()`, `writeState()` |
|
|
309
|
+
| `inference.ts` | Claude CLI subprocess calls | `inferText()` |
|
|
310
|
+
| `utils.ts` | Formatting, ID generation | `nowIso()`, `generateContextId()`, `slugify()` |
|
|
311
|
+
| `git-state.ts` | Git snapshot | `captureGitState()` |
|
|
312
|
+
| `subprocess-utils.ts` | Recursive call guard | `isInternalCall()` |
|
|
313
|
+
| `stop-words.ts` | Word list for ID generation | Used by `utils.ts` internally |
|
|
314
|
+
|
|
315
|
+
### `context/` — Context State Management
|
|
316
|
+
|
|
317
|
+
| File | Purpose | Key Exports |
|
|
318
|
+
|------|---------|-------------|
|
|
319
|
+
| `context-store.ts` | CRUD for context state + index | `getContextBySessionId`, `bindSession`, `maybeActivate`, `saveState`, `createContext` |
|
|
320
|
+
| `context-selector.ts` | Route prompts to contexts | `determineContext()`, `BlockRequest` |
|
|
321
|
+
| `context-formatter.ts` | Display formatting | `formatContextSummary()` |
|
|
322
|
+
| `plan-manager.ts` | Plan lifecycle (archive, hash, sign) | `archivePlan()`, `computePlanHash()` |
|
|
323
|
+
| `task-tracker.ts` | Task CRUD on state.json | `addTask()`, `updateTask()`, `getTasks()` |
|
|
324
|
+
|
|
325
|
+
### `handoff/` and `templates/`
|
|
326
|
+
|
|
327
|
+
| File | Purpose | Key Exports |
|
|
328
|
+
|------|---------|-------------|
|
|
329
|
+
| `handoff/document-generator.ts` | Handoff document generation | `generateHandoffDocument()` |
|
|
330
|
+
| `templates/formatters.ts` | Display constants, mode maps, icons | `MODE_MAP`, `STATUS_ICONS` |
|
|
331
|
+
| `templates/plan-context.ts` | Plan evaluation templates | `PLAN_EVALUATION_REMINDER` |
|
|
332
|
+
|
|
333
|
+
### Root
|
|
334
|
+
|
|
335
|
+
| File | Purpose |
|
|
336
|
+
|------|---------|
|
|
337
|
+
| `types.ts` | All shared types: `Mode`, `ContextState`, `Task`, `HookInput`, `HookOutput` |
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Shared Hooks (`_shared/hooks-ts/`)
|
|
342
|
+
|
|
343
|
+
These run for ALL templates. Method-specific hooks live in `_{method}/hooks/`.
|
|
344
|
+
|
|
345
|
+
| Hook | Event | Purpose |
|
|
346
|
+
|------|-------|---------|
|
|
347
|
+
| `user_prompt_submit.ts` | UserPromptSubmit | Context enforcement — binds prompts to tracked contexts |
|
|
348
|
+
| `context_monitor.ts` | PostToolUse:* | Context window tracking, handoff warnings at 30/20/10% |
|
|
349
|
+
| `session_start.ts` | SessionStart | Restores plan/handoff context after `/clear` or compaction |
|
|
350
|
+
| `session_end.ts` | SessionEnd | Stages `active` --> `has_plan`/`has_handoff` for next session |
|
|
351
|
+
| `archive_plan.ts` | PreToolUse:ExitPlanMode | Archives plan file before accept/reject decision |
|
|
352
|
+
| `pre_compact.ts` | PreToolUse:Compact | Pre-compaction state snapshot |
|
|
353
|
+
| `task_create_capture.ts` | PostToolUse:TaskCreate | Persists task creation to context state |
|
|
354
|
+
| `task_update_capture.ts` | PostToolUse:TaskUpdate | Persists task updates to context state |
|
|
355
|
+
| `file-suggestion.ts` | PostToolUse:Write | Suggests file organization improvements |
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Environment Variables
|
|
360
|
+
|
|
361
|
+
| Variable | Effect |
|
|
362
|
+
|----------|--------|
|
|
363
|
+
| `CLAUDE_PROJECT_DIR` | Override project root detection |
|
|
364
|
+
| `HOOK_LOG_DISABLE=1` | Disable all file logging |
|
|
365
|
+
| `HOOK_LOG_LEVEL=warn` | Minimum log level (default: `debug`) |
|
|
366
|
+
| `HOOK_ERROR_LOG_DISABLE=1` | Legacy alias for `HOOK_LOG_DISABLE` |
|
|
367
|
+
| `_CC_INTERNAL=1` | Marks subprocess calls (checked by `isInternalCall()`) |
|