rules-to-hook
Hook Context Rules
Author and maintain .claude/context-rules.json for the context-inject.mjs hook.
The engine supports both Claude Code and VS Code Copilot hook payload formats.
VS Code Copilot support
The engine auto-detects payloads from VS Code Copilot (camelCase fields, toolArgs
as JSON string). Key differences from Claude Code:
- Event name: VS Code doesn't include the event in the payload. Pass it as a
CLI argument:
node .claude/hooks/context-inject.mjs preToolUse. The engine normalizes camelCase (preToolUse) to PascalCase (PreToolUse) used in rules. - Tool input: VS Code sends
toolArgs(JSON string), nottool_input(object). The engine parses it automatically. - Output: VS Code only supports
permissionDecisiononpreToolUse— flat JSON, nohookSpecificOutputwrapper. Context injection (text,hint,learnings) has no effect on VS Code. - Supported inject types on VS Code: only
blockandallow(PreToolUse).
VS Code hook configuration
{
"hooks": {
"preToolUse": [{
"type": "command",
"bash": "node .claude/hooks/context-inject.mjs preToolUse"
}],
"postToolUse": [{
"type": "command",
"bash": "node .claude/hooks/context-inject.mjs postToolUse"
}]
}
}
Config Format
Rules are a JSON array. Each rule has an on event, a when filter (all keys optional,
implicit AND), and an inject payload (exactly one key).
[
{
"on": "PreToolUse",
"when": {
"tool": "Write|Edit|MultiEdit|replace_string_in_file|create_file|multi_replace_string_in_file",
"path": "src/core/**"
},
"inject": {
"text": "core/ is wiring only — no business logic here."
}
},
{
"on": "PostToolUse",
"when": { "tool": "Read|read_file", "path": "src/stages/**" },
"inject": { "hint": "docs/stages-overview.md" }
}
]
Block and allow rules control tool execution (PreToolUse only):
[
{
"on": "PreToolUse",
"when": { "tool": "Bash", "command": "rm -rf" },
"inject": { "block": "Destructive rm -rf is not allowed." }
},
{
"on": "PreToolUse",
"when": { "tool": "Read", "path": "docs/**" },
"inject": { "allow": "Safe: reading documentation." }
}
]
Precedence: block > allow > context. If any matched rule is a block,
the tool call is denied even if other rules allow or inject context.
on values
PreToolUse / PostToolUse / UserPromptSubmit / SessionStart / SubagentStart / PostToolUseFailure / Stop / PreCompact
The installer registers hooks for all 8 events automatically.
when keys (all optional, implicit AND)
| Key | Type | Notes |
|---|---|---|
tool |
string | Pipe-separated alternatives: "Edit|Write" |
path |
glob | Matched against file_path / path in tool input |
command |
regex | Bash tool only — matched against command string |
prompt |
regex | UserPromptSubmit only |
source |
string | Pipe-separated; SessionStart source field |
agent_type |
string | Pipe-separated; SubagentStart agent type |
error |
regex | PostToolUseFailure error message |
content |
regex | Write content or Edit new_string field |
response |
regex | Matched against JSON.stringify(tool_response) |
Platform Tool Names
Claude Code and VS Code Copilot use different tool names. Always include both
sets in tool patterns to ensure rules fire on both platforms.
| Action | Claude Code | VS Code Copilot |
|---|---|---|
| Write | Write |
create_file |
| Edit | Edit |
replace_string_in_file |
| Multi-edit | MultiEdit |
multi_replace_string_in_file |
| Read | Read |
read_file |
| Shell | Bash |
run_in_terminal |
For write rules: Write|Edit|MultiEdit|replace_string_in_file|create_file|multi_replace_string_in_file
For read rules: Read|read_file
inject keys (exactly one)
| Key | Behavior | Events | VS Code |
|---|---|---|---|
text |
Injected verbatim as additionalContext |
All | No effect |
hint |
Prefixed with "Related: " — agent decides whether to read |
All | No effect |
learnings |
Injects file-matched learnings from .claude/learnings.json |
All | No effect |
block |
Denies the tool call or blocks the event; value is the reason | PreToolUse, PostToolUse, UserPromptSubmit, Stop | Works (PreToolUse only) |
allow |
Auto-approves the tool call; value is the reason shown to user | PreToolUse only | Works |
Block behavior per event
| Event | Block output | Notes |
|---|---|---|
PreToolUse |
hookSpecificOutput.permissionDecision: 'deny' |
Backward-compatible; denies the tool call |
PostToolUse |
{ decision: 'block', reason, hookSpecificOutput } |
Blocks with optional context |
UserPromptSubmit |
{ decision: 'block', reason, hookSpecificOutput } |
Blocks with optional context |
Stop |
{ decision: 'block', reason } |
Prevents agent from stopping (no HSO) |
Stop safety guard
When a Stop hook blocks (meaning "don't stop, continue"), the agent retries stopping
with stop_hook_active: true in the payload. The engine detects this flag and exits
silently (process.exit(0)) before matching rules, allowing the agent to stop and
preventing infinite loops.
Learnings System
Agents can record file-bound learnings that are injected as context when other agents interact with those files.
Storage
Learnings are stored in .claude/learnings.json:
[
{
"files": ["src/stages/**"],
"learning": "Stage transforms must use AST nodes, not string manipulation",
"timestamp": "2026-03-04T10:30:00Z"
}
]
Recording learnings (CLI)
# Add a learning
node .claude/hooks/learn.mjs add --files 'src/stages/**' --learning 'Use AST nodes only'
# Add for multiple file patterns
node .claude/hooks/learn.mjs add --files 'src/core/**,src/stages/**' --learning 'Cross-cutting concern'
# List all learnings
node .claude/hooks/learn.mjs list
# List learnings matching a file
node .claude/hooks/learn.mjs list --files 'src/stages/elision.ts'
# Remove by index
node .claude/hooks/learn.mjs remove --index 0
# Update by index
node .claude/hooks/learn.mjs update --index 0 --learning 'Updated insight'
# Update file globs after rename/move
node .claude/hooks/learn.mjs repath --index 0 --files 'src/new-path/**'
# Find orphaned learnings (globs that match no files)
node .claude/hooks/learn.mjs check
When adding, the CLI shows overlapping existing learnings so you can curate (merge, update, or remove redundant entries).
When renaming or deleting files, run check to find orphaned learnings,
then repath or remove to fix them.
Injection
The learnings inject type in a context rule triggers lookup:
{
"on": "PostToolUse",
"when": { "tool": "Read|read_file", "path": "**" },
"inject": { "learnings": true }
}
When matched, the engine reads .claude/learnings.json, finds entries whose files
globs match the current file path, and injects them as additionalContext:
[Learnings for src/stages/elision.ts]
- Stage transforms must use AST nodes, not string manipulation
- Elision uses line-count thresholds
Prompt reminders
A UserPromptSubmit context rule reminds agents to record learnings on every
prompt. It has no when filter, so it fires unconditionally. This rule is
installed automatically by the installer.
Phase 0 — Bootstrap
Runs before any other phase, every time the skill is invoked.
Detection
Check whether the hook engine is installed:
.claude/hooks/context-inject.mjsexists.claude/hooks/platform.mjsexists.claude/hooks/learn.mjsexists.claude/hooks/harness-eval.mjsexists.claude/hooks/harness-format.mjsexists.claude/settings.jsonhascontext-inject.mjsregistered in all 8 events (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SubagentStart, PostToolUseFailure, Stop, PreCompact).claude/hooks/node_modules/minimatchexists (dependency installed).claude/learnings.jsonexists
If all eight pass → skip to Phase 1.
If any is missing → run the installer:
node skills/rules-to-hook/install.mjs
The installer is idempotent. It:
- Creates
.claude/hooks/and copies the engine fromskills/rules-to-hook/engine.mjs - Copies the platform module from
skills/rules-to-hook/platform.mjs - Copies the learn CLI from
skills/rules-to-hook/learn.mjs - Copies harness modules from
skills/rules-to-hook/harness-eval.mjsandskills/rules-to-hook/harness-format.mjs - Ensures
package.jsonhasminimatchand installs dependencies - Merges hook registrations into
.claude/settings.json(all 8 events,.*matcher) - Seeds an empty
.claude/context-rules.jsonif absent - Seeds an empty
.claude/learnings.jsonif absent - Seeds learnings context rules (injection on Read + prompt reminder) into
context-rules.json
After the installer completes, confirm: "Hook engine installed. Proceeding to auto-discovery."
If the installer fails, stop and report the error — do not continue to later phases.
Phase 1 — Auto-discovery
Runs by default (and always first, even when invoked with add).
Discovery proceeds in five layers dispatched to subagents to keep the main context clean. Only structured summaries flow back. Do not propose rules until all layers complete.
Dispatch plan
Batch 1 (parallel): Layer 1 · Layer 2 · Layer 3
Batch 2 (parallel): Layer 3.5 · Layer 4 — both need Layer 3 output
Batch 3 (sequential): Layer 5 — needs Layer 3.5 + Layer 4 output
Create a tracked task for each batch (not each layer).
Batch 1 — Gather project data (3 parallel subagents)
Dispatch three general-purpose Task subagents in a single message.
Each prompt must include the project root path and end with:
"Return a structured summary only — not raw file contents."
Subagent → Layer 1: Project identity & toolchain
Prompt must instruct the subagent to:
- Glob
*.mdat project root and read each file — extract purpose, guidelines, invariants. - Read the package manifest (
package.json,pyproject.toml,Cargo.toml, or equivalent) — extract scripts, runtime, key deps. - Read config files (
biome.json,.eslintrc*,tsconfig.json,prettier.config.*, etc.) — note codified conventions.
Expected return:
- Purpose (one sentence)
- Runtime & package manager
- Key dependencies (name → role)
- Codified conventions (list)
Subagent → Layer 2: Architecture map
Prompt must instruct the subagent to:
- Run
npx @anduril-code/ctx tree --depth 3— identify source, doc, config, and test dirs. - Glob
src/**/index.{ts,js}(or language equivalent) — read barrel files for module boundaries and public surfaces. - Check for
types/,interfaces/, or similar dirs — confirm whether pure declarations (no runtime code, no cross-imports).
Expected return:
- Directory roles (table: dir → purpose)
- Module boundaries (what each barrel exports)
- Type-only directories (if any)
Subagent → Layer 3: Documentation deep-dive
Prompt must instruct the subagent to:
- Run
npx @anduril-code/ctx rank "architecture constraints invariants style" --maxResults 10. Read every result — do not guess from filenames. - Glob
docs/**/*.mdand read all remaining docs. For each doc: a. Usenpx @anduril-code/ctx sections <file>to list headings. b. Usenpx @anduril-code/ctx extract <file> --onlySections '<heading>'to read each section individually. c. For each section that contains actionable rules or constraints, produce a section record:section— the heading namefile— source doc pathcontent— condensed actionable rules (max 500 chars, drop examples/rationale)keywords— 3-5 code-relevant terms (identifiers, directory names, patterns) that would appear in files this section governs
- Skip sections that are purely informational (overview, changelog, credits).
Expected return:
- Array of section records (JSON), each with
section,file,content,keywords
Wait for all three subagents. Collect their outputs.
Batch 2 — Path affinity + enforcement audit (2 parallel subagents)
Dispatch two general-purpose Task subagents in a single message.
Paste the section records from Layer 3 into both prompts.
Subagent → Layer 3.5: Path affinity via codebase grep
Prompt must instruct the subagent to:
- For each section record, Grep the codebase for each keyword.
- For each keyword hit, note which top-level source directory contains it
(e.g.
src/api,src/components,tests). - Rank directories by hit density: hits for this section's keywords divided by total files in the directory.
- Assign the top 1-3 directories as path globs (e.g.
src/api/**). - If a section's keywords match across 5+ top-level directories with no
clear winner (no directory has >30% of hits), assign
src/**and flag asbroad: true. - If a section's keywords produce zero hits, flag as
noMatch: true.
Expected return:
- Enriched section records: original fields plus
paths(string array of globs) and optionalbroad/noMatchflags
Subagent → Layer 4: Enforcement audit
Prompt must instruct the subagent to:
- Read every file in
.claude/hooks/— source code, not just filenames. Understand what each hook enforces (blocks, rewrites, denies, injects). - Read
.claude/context-rules.jsonif it exists — list every rule. - Cross-reference each section record from Layer 3 against existing
hooks and rules. Mark each as:
- Enforced — already covered → skip
- Unenforced — no coverage → candidate
- Partial — partially covered → may need complementary rule
Expected return:
- Existing hooks summary (what each enforces)
- Existing rules list
- Section status table (section → Enforced / Unenforced / Partial)
Wait for both subagents. Collect their outputs.
Batch 3 — Verify & derive candidates (1 subagent)
Dispatch one general-purpose Task subagent.
Paste the enriched section records from Layer 3.5 and the section status
table from Layer 4 into the prompt.
Subagent → Layer 5: Verification & per-section rule generation
Prompt must instruct the subagent to:
- Filter to unenforced / partial sections only (from Layer 4 status).
- For every section that asserts a codebase property (e.g. "types/ is pure", "no imports from cli/"), use Grep to confirm it actually holds. Drop any section whose assertions do not hold.
- For each verified section, emit one rule per path in its
pathsarray:{ "on": "PreToolUse", "when": { "tool": "Write|Edit|MultiEdit|replace_string_in_file|create_file|multi_replace_string_in_file", "path": "<glob>" }, "inject": { "text": "<Section Name>: <condensed content>" } }- Prefix
textwith the section name for self-describing context. - Cap
textat 500 characters. If longer, condense to actionable rules only (drop examples and rationale).
- Prefix
- For each verified section, also emit one PostToolUse Read rule per
path in its
pathsarray:{ "on": "PostToolUse", "when": { "tool": "Read|read_file", "path": "<glob>" }, "inject": { "hint": "<most relevant source doc path>" } }- Use
hint(nottext) to keep Read injections lightweight.hintis prefixed with"Related: "— the agent decides whether to follow up. - The
hintvalue should be the relative path to the most relevant source doc from the section's_sourcefield. - Apply the same deduplication as step 5: if multiple sections target the same path glob, pick the single most relevant doc to hint rather than emitting multiple Read rules for the same path.
- Use
- Deduplication: if two sections produce rules with the same path and overlapping content, merge them into one rule with combined text (separated by newline).
- For sections flagged
noMatch: true, include the candidate but annotate with(no matching code found)— the user will assign a path or skip. - For sections flagged
broad: true, include the candidate but annotate with(broad — keywords matched everywhere). - Format each candidate as a JSON rule object.
Expected return:
- Verified sections (with Grep evidence)
- Candidate rules (JSON array, each annotated with source section, file, path, and any flags)
Wait for completion.
Presentation
Use the Layer 5 output to present a numbered table of candidate rules. Each row must include:
- Scope — the path glob this rule targets
- The rule (compact JSON or summary)
- Source — which doc/file and section established the constraint
- Verification — what Grep/check confirmed it
- Flags —
broadorno matchif applicable
# on scope inject source
───────────────────────────────────────────────────────────────────────────
0 PreToolUse src/api/** text: "API Design: Use camel…" docs/style.md §API Design
1 PostToolUse src/api/** hint: docs/style.md docs/style.md §API Design
2 PreToolUse src/components/** text: "Components: Functional…" docs/style.md §Components
3 PostToolUse src/components/** hint: docs/style.md docs/style.md §Components
4 PreToolUse **/*.test.* text: "Test Patterns: descri…" docs/style.md §Test Patterns
5 PostToolUse **/*.test.* hint: docs/style.md docs/style.md §Test Patterns
6 PreToolUse src/** (broad) text: "Naming: Use snake_cas…" docs/style.md §Naming
7 PostToolUse src/** (broad) hint: docs/style.md docs/style.md §Naming
Ask: "Accept all (a), pick by number (e.g. 1,3), or skip (s)?"
For rules flagged (no match), prompt: "No matching code found for
''. Assign a path glob or skip? (glob / s)"
Write accepted rules to .claude/context-rules.json (create if absent,
merge if exists).
Phase 2 — User Additions
After auto-discovery (or when invoked with add), ask: "Anything else to add? (y/n)"
If yes, collect one answer at a time:
-
Event — PreToolUse / PostToolUse / UserPromptSubmit / SessionStart / SubagentStart / PostToolUseFailure / Stop
-
Trigger keys — show only valid keys for the chosen event:
- PreToolUse / PostToolUse:
tool,path,command - UserPromptSubmit:
prompt
- PreToolUse / PostToolUse:
-
Inject type — text / hint / block / allow
- For
block: available on PreToolUse, PostToolUse, UserPromptSubmit, Stop - For
allow: only available when event is PreToolUse
- For
-
Inject content:
- For
hint: list existing.mdfiles nearby and suggest one
- For
-
Preview the JSON rule and ask: "Add this rule? (y/n)"
-
On confirm, append to
.claude/context-rules.json.
Repeat until user says no.
Phase 3 — List
When invoked with list, display current rules as an ASCII table:
# on when inject
─────────────────────────────────────────────────────────────────
0 PreToolUse tool=Edit|Write path=src/** text: "..."
1 PostToolUse tool=Read path=src/stages/** hint: docs/...
2 UserPromptSubmit prompt=test text: "..."
If .claude/context-rules.json does not exist, print: No rules found.
Phase 4 — Remove
When invoked with remove <index>:
- Show the full rule at that index.
- Ask: "Remove this rule? (y/n)"
- On confirm, remove the entry and rewrite the file.
If the index is out of range, print: Index <n> is out of range (0–<max>).
Phase 5 — Smoke Test
Runs automatically after writing rules in Phase 1 or Phase 2. For each accepted rule, verify the hook engine actually fires by simulating a matching tool call.
Procedure
-
For each rule in
.claude/context-rules.json, construct a minimal matching payload:{ "hook_event_name": "<rule.on>", "tool_name": "<first alternative from rule.when.tool>", "tool_input": { "file_path": "<synthetic path matching rule.when.path>" } }- For
when.tool: use the first pipe-separated alternative (e.g."Write"from"Write|Edit|MultiEdit|..."). - For
when.path: expand the glob to a concrete path (e.g."src/core/example.ts"for"src/core/**"). - For
when.command: settool_input.commandto a string matching the regex. - For
when.prompt(UserPromptSubmit): set"prompt"to a matching string instead oftool_name/tool_input.
- For
-
Pipe the payload to the engine and capture stdout:
echo '<payload>' | node .claude/hooks/context-inject.mjs -
Parse the JSON output and check:
- context rules (
text/hint): output must containadditionalContextwith the expected value. - block rules: output must contain
"permissionDecision": "deny". - allow rules: output must contain
"permissionDecision": "allow". - no output (exit 0, empty stdout): rule did not fire — fail.
- context rules (
-
Also construct one non-matching payload per rule (wrong tool name or path outside the glob) and confirm the engine produces no output (empty stdout or exit 0). This guards against overly broad rules.
Reporting
Present results as:
Rule on scope inject result
──────────────────────────────────────────────────
0 PreToolUse src/api/** text PASS ✓ (matched, context injected)
1 PostToolUse src/api/** hint PASS ✓ (matched, hint injected)
2 PreToolUse src/api/** text FAIL ✗ (no output — rule did not fire)
If any rule fails:
- Show the payload that was sent and the raw output received.
- Suggest likely causes: wrong
toolname, path glob mismatch, or missing config file. - Ask: "Fix the failing rules and re-test? (y/n)"
If all rules pass: "All rules verified — hooks are firing correctly."
Validation
Apply before writing any rule:
-
whenmust have at least one key — reject rules with an emptywhenobject. Exception:UserPromptSubmitrules may omitwhenentirely to fire on every prompt (the engine treats missingwhenas{}, matching unconditionally). -
pathglobs must be non-empty strings. -
hintvalues should exist on disk — warn if the file is not found, but do not block. -
injectmust have exactly one key — reject if zero or more than one key is present. -
blockis valid onPreToolUse,PostToolUse,UserPromptSubmit, andStop— reject on other events. -
allowis only valid onPreToolUse— reject on other events with:"allow rules only work on PreToolUse events." -
sourcekey is only meaningful onSessionStart;agent_typeonSubagentStart;erroronPostToolUseFailure.
The engine also enforces this validation at runtime. Invalid rules are ignored instead of being partially applied.
Notes
-
Prefer
hintovertextfor long or optional context — it costs fewer tokens. -
The hook fires on every matching tool call — keep
whenfilters targeted. -
Phase 0 may create
context-inject.mjsand modifysettings.json— only during initial bootstrap.