SLOPSHOPPER

rule-fairy

Repository rules and docs, delivered to your coding agent. Injects Cursor-style rules into Claude Code sessions by prompt keyword, edited path, or alwaysApply…

newguardpromptprocess
★ 2v0.3.0MITupdated 2026-10-09Cyrik/rule-fairy
A shopper browsing a rack in a slop shop
README

Rule Fairy

Repository rules and docs, delivered to your coding agent.

Rule Fairy is a plugin for Claude Code and Codex. It reads the rules a repository already keeps for Cursor (.cursor/rules/**/*.mdc, or any directories you configure) and injects the relevant ones into the agent's context: when a prompt mentions a rule's keywords, when the agent edits a file the rule's globs match, or on the first prompt for rules marked alwaysApply. Rules can import sections of your documentation by heading, and injected rules come back after the context is compacted. The same matcher powers a review of a diff or a plan against the rules that apply to it.

Clojure/conj 2026 talk: Just-in-Time Context for Agents (slides, PDF).

Why not the harnesses' own rules

Claude Code has .claude/rules/ with paths: globs, Codex has AGENTS.md, Cursor has .cursor/rules/. Rule Fairy exists for what they leave out:

  • One rule source for three tools. The same rule files serve Cursor natively and Claude Code and Codex through the plugin. The review reads the harness instruction files as well, so nothing has to be copied between AGENTS.md, CLAUDE.md and the rules.
  • Prompt keyword triggers. A rule can fire when the user mentions its topics (promptAnyOf, promptRequires), not only when a file is touched.
  • Firing on edits. Claude Code's path-scoped rules load when a matching file is read, not when it is written or edited (anthropics/claude-code #23478, #38487, #95083). Rule Fairy delivers the matching rules with the result of an Edit or Write on Claude Code and right after one on Codex.
  • Heading-level documentation imports. @doc/guide.md#section pulls one section of a larger document into a rule; overlapping imports merge and <!-- agent-context: omit --> blocks stay out. Native imports take whole files. The rendered rule keeps a line where the import stood, naming the reference, because the section itself is delivered in the Required documentation blocks, possibly in another shard.
  • Reinjection after compaction. After /compact, Claude Code re-injects only the root CLAUDE.md, and path-scoped rules return only when a matching file is read again. Rule Fairy reads what the conversation still holds and delivers a matched rule again once a compaction has emptied it; on Codex it counts compactions per session.
  • Review against the rules. Two skills check a diff or a plan against exactly the rules and instruction files that apply to the paths it touches, and report only what a rule supports.

Install

Babashka (bb) must be on the PATH of the process that runs your agent. The hooks look in ~/.local/bin, /opt/homebrew/bin and /usr/local/bin as well, for agents launched from an editor or app with a minimal PATH.

Claude Code 2.1.287 or later, from GitHub or from a local checkout. The Claude Code side of the plugin is a mod, a module Claude Code loads in its own process; an older Claude Code loads no hooks from it and injects nothing, while the review skills still work. Rule Fairy 0.2.3 is the last version with the older settings hooks.

claude plugin marketplace add Cyrik/rule-fairy   # or: claude plugin marketplace add ./rule-fairy
claude plugin install rule-fairy@rule-fairy

Codex:

codex plugin marketplace add Cyrik/rule-fairy
codex plugin add rule-fairy@rule-fairy

Running sessions pick up hooks on restart; a Claude Code session also picks up an install or update made from the shell after /reload-plugins. If the repository still registers the older project-local hooks in .claude/settings.json or .codex/hooks.json, remove them so rules are not injected twice.

The review skills are /rule-fairy:review and /rule-fairy:review-plan in Claude Code and $review and $review-plan in Codex. Both also trigger from a plain request such as "review this PR against our rules" or "check this plan against the project conventions". They need bb; reviewing a pull request also needs the GitHub CLI (gh), logged in.

How it works

  • Prompt hook (Claude prompt.submit, Codex UserPromptSubmit): selects every alwaysApply rule plus every rule whose promptAnyOf terms appear in the prompt (all promptRequires terms must appear too), renders them with their documentation imports, and injects them with the prompt. On Claude Code a prompt the person did not write, such as a background task's report, another session's message or another plugin's prompt, selects nothing: keyword rules match what the person asked.
  • Edit hook (Claude tool.call on Edit, Write, MultiEdit, NotebookEdit and MCP edit tools, Codex PostToolUse): matches the edited path against every rule's globs and injects the matches. On Claude Code the rules arrive with the tool's result; on Codex the injection stops the planned turn so the model considers the rules before moving on.
  • Shell hook (Claude tool.call on Bash, Codex PostToolUse on Bash): a shell command can write files no tool input names, such as a heredoc, sed -i or mv. After the command the hook asks git which files changed since the session's last shell check, matches them against the globs and injects the rules with the post-edit contract: apply them on the next pass, revise the change where it conflicts. The check advances with every shell command and starts at the first prompt. The check reads the working tree, so a commit, checkout, rebase or pull that moves HEAD brings nothing, and a file written and committed in the same command is not seen. A change stays pending until its rules have reached the agent, so a rule that fails to build or a delivery cut short is retried by the next command. On Claude Code, the injected context names the changed paths that fit a 1,000-character budget and counts the rest, so a rebase's hundreds of paths stay one line. Outside a git checkout nothing is detected. On by default for Claude Code and off for Codex; :shell-hook in rule-fairy.edn switches either (see Configuration).
  • Dedup: on Claude Code a rule is delivered when no whole delivery of it is in the conversation the model holds, read at each event, and none was delivered in the last minute, which covers tool calls that run side by side. A compaction empties the conversation of past deliveries, so the next matching event delivers again; a forked session starts with what it inherited; no transcript file is read. A delivery counts only when every one of its entries, told apart by the delivery id in the shard header, is present. On Codex one state file per session records the transcript metrics at each injection, and a rule is injected again after a compaction or after 2 MiB of transcript growth as a fallback for silent context cleanup. State lives under .rule-fairy/<harness>/ in the project and ignores itself in git.
  • Bounded delivery: on Claude Code one delivery is at most two context entries of 100,000 characters each, which the model reads whole; a settings hook's output is cut at 10,000 characters (anthropics/claude-code#94358), which is why the Claude Code side is a mod. A matched set that needs more entries than that, or more documentation than the 64 KiB budget, is delivered in match order as far as it fits; the first entry names the rest, which comes with the next event that matches it. A single rule too large for either is reported and never recorded. Codex caps additionalContext at 2,500 tokens by default; the registration sets additionalContextLimit to 0.
  • Failures are visible: on Claude Code a delivery that fails, its process exiting non-zero or the hook itself throwing or timing out, attaches one error entry naming the failure and logs a line to the transcript, so the model and the person both see it, and the prompt or tool goes on as if unhooked. Once a prompt has entered the session no context can attach any more, so a failure after that point is logged only.
  • Review skill (rule-fairy:review, both harnesses): fetches a diff (a pull request through gh, uncommitted work, or a branch against its base), builds a bundle of the instruction files, rules and documentation that apply to the changed paths with duplicates collapsed, and reviews the diff against it. On Claude Code the review runs in the read-only rule-fairy:rules-reviewer agent; on Codex the skill follows the same procedure inline. Findings cite the rule; nothing is edited, committed or posted.
  • Plan review skill (rule-fairy:review-plan, both harnesses): the same check before any code exists. It works out which files a plan will create or edit, from a file-list section or from the paths the text names, builds the bundle for those paths, and judges each step against it. Reruns after the plan changes.

Layout

  • .claude-plugin/plugin.json, .codex-plugin/plugin.json: the manifests. .claude-plugin/ is Claude Code's; .codex-plugin/ is Codex's, carrying the hooks registration and the listing interface, with skills/ discovered by convention. There is deliberately no portable root plugin.json: Codex CLI 0.154.0 loads no hooks at all from a package that has one, whether they are declared in the overlay, in the root file or in its extensions.com.openai block, although its docs say the overlay applies. .claude-plugin/marketplace.json makes the repository its own marketplace for both harnesses. The three files carry the same version, bumped with every push meant for installed users, because Claude Code's plugin update acts only on a version change.
  • hooks/hooks.json, hooks/codex-hooks.json: the Claude Code hooks manifest names the mod's module; the Codex one registers commands. Every process the hooks start runs hooks/run, a small shell wrapper that finds the plugin root, hands it to the script as RULE_FAIRY_PLUGIN_ROOT, makes sure bb is on the PATH, and starts Babashka with the plugin's own bb.edn so a consuming repository's bb.edn never reaches the classpath.
  • hooks/claude/: register.ts, the mod, one in-process hook per event that runs mod.bb once per delivery and attaches what it prints; mod.bb, the entry point, which selects the rules for a prompt, an edit or a shell command; common.bb (project root, session state, the delivery, the glob cache, shell-change detection); common_test.bb; and register.test.ts, the mod's tests under claude plugin test.
  • hooks/codex/: common.bb, prompt.bb, post_edit.bb (edits and shell commands alike), and common_test.bb. The adapters intentionally have different delivery contracts.
  • src/rule_fairy/rules.clj: the engine. Rules source, frontmatter parsing, glob and prompt matching, documentation imports with overlap merging, rendering, and block splitting. markdown.clj holds the line-level Markdown structure it shares with the bundle code (fences, headings, sections, units); config.clj reads rule-fairy.edn.
  • src/rule_fairy/instructions.clj: harness instruction files for the review bundle. AGENTS.md, CLAUDE.md and relatives per touched directory, @path imports the way Claude Code follows them, block comments stripped, one copy per real file. src/rule_fairy/dedup.clj: section and paragraph deduplication across a bundle, each omission replaced by a line naming the kept copy.
  • src/rule_fairy/session_state.clj: per-session state shared by the adapters: the shell-check marker and pending paths for both, Codex's transcript metrics, Claude Code's delivery times. src/rule_fairy/transcript.clj: incremental compaction counting with a scan cursor for Codex, and the reader that names the rules the whole deliveries among a set of frames injected, for Claude Code. src/rule_fairy/changes.clj: the files changed in a checkout since a moment, from git status plus modification and inode change times, which is how the shell hook finds what a command wrote.
  • src/rule_fairy/bundle.clj: the review bundle. Instruction files, matching and always-on rules and their documentation for a path list, deduplicated, rendered with a header naming what went in, and written under the self-ignored .rule-fairy/review/<mode>/, plus the rules revision recorded next to it. plan.clj extracts the paths a plan intends to touch; git.clj is the small amount of git the review scripts share.
  • skills/review/: the rule-fairy:review skill, discovered by both harnesses from this directory. SKILL.md is what the agent follows; scripts/run starts scripts/review.bb with the plugin's own bb.edn. review fetch takes a pull request through gh, the working tree against HEAD, or everything since the merge base with a branch, and writes patch.diff with a meta.json naming the code revision, the rules revision and the changed paths. The working-tree modes stage the tree into a throwaway index for one diff, so staged, unstaged and untracked changes appear once each and renames made outside git are detected. review bundle reads the patch and writes the rules bundle; review clean removes the run's artifacts. references/procedure.md is the evidence discipline the reviewer follows.
  • skills/review-plan/: the rule-fairy:review-plan skill. review-plan paths <plan> shows which files the plan touches and which tokens were dropped; review-plan bundle <plan> [--paths ...] writes the plan bundle and its meta.json under .rule-fairy/review/plan/; review-plan clean removes them. It shares the procedure file with the review skill.
  • agents/rules-reviewer.md: the read-only reviewer Claude Code runs for both skills. Codex plugins have no agents, so there the skills review inline.
  • test/: engine, Markdown, instruction-file, dedup, bundle, git, plan, transcript, session-state, changes and plugin-layout suites.
  • reference/: two rules documenting the hook machinery and the .mdc format for rule authors.

Configuration

Rules are read from .cursor/rules/**/*.mdc by default, so a repository that already uses Cursor rules needs nothing. An optional rule-fairy.edn at the project root adds or moves directories and extensions:

{:rules {:dirs [".cursor/rules" "docs/rules"]
         :extensions [".mdc" ".md"]}}

Any file under any listed directory with any listed extension is a rule. Directories are relative to the project root; both keys are optional and anything else is rejected. Rule names are relative to their own directory, so the same name in two directories is an error. The frontmatter fields stay Cursor's (description, globs, alwaysApply) plus the extensions promptAnyOf and promptRequires. The format is documented for rule authors in reference/mdc.mdc.

The same file switches the shell hook per harness. It is on for Claude Code and off for Codex unless set:

{:shell-hook {:claude true :codex true}}

Codex makes far more shell calls than edits and rarely edits through the shell, so the check would cost every call for a rare catch; a repository where Codex does write through the shell turns it on here. A switched-off hook still starts, because the plugin's registration is fixed, but exits before any git call.

Cursor itself is assumed to keep working with these files, but it is not fully supported at the moment. Cursor ignores the two extension fields and includes @path imports whole rather than by heading; what it does with a heading import it cannot resolve, and whether saving a rule in its editor strips unknown fields, has not been checked. If you rely on Cursor, keep your rules valid for it and treat the extensions as Rule Fairy's.

The review bundle also reads harness instruction files. By default it looks for AGENTS.override.md, AGENTS.md, .claude/AGENTS.md, CLAUDE.md, .claude/CLAUDE.md and CLAUDE.local.md in the project root and in every directory above a changed file, follows their @path imports the way Claude Code does, and includes each real file once. That is the union of what either harness reads, with one exception: an AGENTS.md beside an AGENTS.override.md is left out when a CLAUDE file exists at the root or in that directory, because Codex reads only the override and Claude Code then reads only its CLAUDE files. To restrict or reorder them:

{:instructions {:files ["AGENTS.md" "CLAUDE.md"]}}

An empty vector includes no instruction files. Order matters: when two files share a section or a long paragraph, the earlier one keeps it and the later one gets a line naming where the kept copy is.

The hooks write session state and glob caches under .rule-fairy/claude/ and .rule-fairy/codex/ in the project, and review bundles go under .rule-fairy/review/<mode>/. Each of those directories carries a .gitignore ignoring its own content, so nothing needs to be added to the repository's ignore rules. The hooks also record the directory the plugin runs from in .rule-fairy/<harness>/plugin-root, one absolute path on one line, for a skill of the project that wants the plugin's review scripts (see Using the review from another skill).

Context injected by the hooks is labelled [rule-fairy injected: <rule>], [rule-fairy matched: alwaysApply ...], [rule-fairy matched: keyword ...], [rule-fairy matched: glob on <path>], [rule-fairy shard n/m <delivery>], and [rule-fairy error: ...].

Using the review from another skill

A project's own review skill can run the rules pass with the plugin's scripts instead of collecting rules itself. The hooks record where the plugin runs from in .rule-fairy/<harness>/plugin-root (claude or codex), one absolute path, written on the first prompt of a session. Read it; a missing file, or one naming a directory that no longer exists, is a setup error to report, not a reason to look for the plugin elsewhere. Under that root, skills/review/scripts/run is the launcher and skills/review/references/procedure.md the procedure. From the project root:

"<root>/skills/review/scripts/run" fetch --base main   # or --local, or --pr <n> [--checkout]
"<root>/skills/review/scripts/run" bundle
"<root>/skills/review/scripts/run" clean                # once every pass has read the artifacts

fetch writes .rule-fairy/review/diff/patch.diff and meta.json (the code revision, the mode and, under rules, the rules revision); bundle writes rules.md beside them and completes rules. A failed command exits non-zero and leaves no new artifact: report it as a coverage failure rather than reuse an older bundle. The commands invoke no skill, post nothing and touch no file of the caller's, with one exception: --checkout checks the pull request out detached and needs the user's explicit authorisation, as the review skill says. clean removes only that directory, when the caller says so. To review, hand a reader the three files and the procedure as absolute paths: in Claude Code the rule-fairy:rules-reviewer agent with the prompt from the review skill, elsewhere a subagent that follows the procedure. It returns findings and coverage to its caller, which builds the report.

Checks

bb test

Runs fifteen Babashka suites: engine, Markdown, instruction files, dedup, bundle, git, plan, transcript, session state, Claude hooks, Codex hooks, the two skills' scripts and the plugin layout, then the mod's TypeScript tests through claude plugin test where Claude Code is installed. Every suite builds its rules, documents, and state in temporary directories; the git and review script suites build real repositories, the latter with a branch, uncommitted and untracked changes, and a fake gh on the PATH. The Claude hook suite runs mod.bb through hooks/run against a temporary project, the way the mod does, and the Codex suite runs the registered commands from hooks/codex-hooks.json through a shell, with a broken bb.edn planted in the project to prove it does not affect the hooks. The registration test reads RULE_FAIRY_SETTINGS_FILE to validate another hooks manifest. The mod's tests answer its session, process and log calls from the test, so no Babashka runs there. The skill script suites run each skill's scripts/run shim as a subprocess, the way the skill text tells an agent to. The layout suite checks that every skill and agent file carries the frontmatter both harnesses need and that the files the skill text names exist. claude plugin validate . lists the mod's hooks and the calls it makes. Loading the plugin once with claude --plugin-dir . makes Claude Code write its type declarations under .claude-plugin/types/, and bunx tsc -p tsconfig.json --noEmit then type-checks the module and its tests against them.

Design decisions

  • Keep project rules and their human documentation in the consuming repository.
  • Separate installed plugin code, the target project/worktree, and writable state. Do not rely on the consumer's Babashka classpath or current directory.
  • Share matching and rendering; retain harness-specific input, transcript, and delivery handling. Do not unify those behaviours merely for packaging. Frontmatter parsing, glob matching, and the rule index live only in the engine; adapters keep their cache files, path relativisation, and output.
  • An invalid glob in a rule fails the hook with the glob named, instead of silently matching nothing. Rule files are user input; the failure is loud on purpose.
  • alwaysApply: true rules are selected by the prompt hooks on every prompt and delivered through the normal session dedup, so they arrive with the first prompt and return after compaction. Consuming repositories should not duplicate that content in CLAUDE.md or AGENTS.md.
  • On Codex, transcript metrics are read incrementally. The session state keeps a scan cursor and each event counts only the complete lines appended since, so the per-prompt cost of the reinjection check does not grow with the session. Claude Code reads no transcript: the mod reads the conversation the model holds, which is the question the metrics approximated.
  • Each harness keeps one dedup map per session shared by all of its hooks. A rule delivered after an edit is not delivered again by the next prompt, and the reverse. Codex previously kept separate prompt and post-edit buckets.
  • The code, labels, state paths and environment variables say "rule" and "rule-fairy"; "MDC" is Cursor's name for the default file format and appears only where that format is meant. The rules location is configuration, not identity: .cursor/rules/**/*.mdc stays the default because Cursor requires it and Claude Code's /init already recognises it.
  • The Claude Code adapter is a mod: one in-process hook per event, one Babashka process per delivery, context entries of up to 100,000 characters. The settings hooks it replaced were cut at 10,000 characters and needed twelve lanes per event, a batch protocol and completion markers to deliver one injection; the bugs that design produced were consequences of the cap. The Codex adapter keeps its settings hooks, whose cap is configurable.
  • Every hook updates its session's state under a per-session file lock. Claude Code runs tool calls, and so their hooks, side by side; Codex launches the hooks matching an event concurrently and runs a subagent's hooks under the parent's session id.
  • An edited path is matched the way the project's globs are written: relative to the checkout that holds the file, less the project's own position in its checkout. A session started in a subdirectory of a repository keeps its `src/*
Source 1 files
hooks/claude/register.ts 161 lines
1// The Claude Code mod of Rule Fairy: a function hook per event in place of
2// the settings hooks, so one delivery is one process and the 10,000-character
3// cap on a settings hook's output no longer applies. Each hook hands its
4// event to hooks/claude/mod.bb through hooks/run and attaches the context
5// entries that prints: after the prompt on prompt.submit, with the tool's
6// result on tool.call. With the event goes the head of every Rule Fairy
7// frame the conversation already holds, read from the Messages API form of
8// the session, so a session that inherited its context, as a fork does,
9// starts knowing what is already delivered. A subagent's tool calls deliver
10// nothing, as the settings hooks delivered nothing there, and nor does a
11// prompt another agent or process composed. A delivery that fails, its
12// process exiting non-zero or the hook itself throwing or timing out,
13// attaches one error entry naming the failure and logs a line to the
14// transcript, so the model and the person both see it, and the prompt or
15// tool goes on as if unhooked. Once a prompt has entered the session no
16// context can attach any more, so a failure after that point is logged only.
17import type { EngineInterface, HookFailure, Register } from 'claude-code'
18
19type Kind = 'prompt' | 'edit' | 'shell'
20
21const EDIT_TOOL = /^(Edit|Write|MultiEdit|NotebookEdit)$|^mcp__.*(edit|write|patch)/
22
23// Prompts the person did not write: a background task's report, another
24// session's message, a coordinator's hand-off, an observer's note or a
25// plugin's own prompt. Keyword rules match what the person asked, so these
26// deliver nothing. The settings hook had no way to tell them apart and
27// matched rules against a subagent's report.
28const COMPOSED_BY_OTHERS = new Set([
29  'task-notification',
30  'peer',
31  'peer-send-message',
32  'projects-relay',
33  'coordinator',
34  'observer',
35  'observer-activity',
36  'plugin',
37])
38
39// A frame as the model reads it: Claude Code wraps a hook's context in a
40// system reminder naming the event, and the frame's shard header and marker
41// lines follow. Only those lines are captured; the rule bodies stay behind.
42const FRAME_HEAD = /<system-reminder>\n[\w.:]+ hook additional context: (\[rule-fairy shard [^\n]*(?:\n\[rule-fairy [a-z]+: [^\n]*)*)/g
43
44// How much of a failed process's error output the entry carries: Babashka
45// prints the error's type, message and location first and the stack last.
46const STDERR_HEAD_CHARS = 1_000
47
48type Block = { type?: string; text?: string; content?: unknown }
49
50function texts(content: unknown): string[] {
51  if (typeof content === 'string') return [content]
52  if (!Array.isArray(content)) return []
53  return (content as Block[]).flatMap(block =>
54    block.type === 'tool_result' ? texts(block.content) : typeof block.text === 'string' ? [block.text] : [],
55  )
56}
57
58export function frameHeads(messages: unknown): string[] {
59  if (!Array.isArray(messages)) return []
60  return (messages as Block[]).flatMap(message =>
61    texts(message.content).flatMap(text => [...text.matchAll(FRAME_HEAD)].map(found => found[1] ?? '')),
62  )
63}
64
65// The one context entry a failed delivery attaches: the failure named the
66// way an oversized rule's entry names its reason, the head of the detail,
67// and the line that tells the model nothing was recorded.
68export function failureEntry(kind: Kind, failure: string, detail: string): string {
69  const head = detail.trim().slice(0, STDERR_HEAD_CHARS)
70  return [
71    `[rule-fairy error: ${kind} delivery ${failure}]`,
72    ...(head === '' ? [] : [head]),
73    'No matched rules were injected or marked as injected.',
74  ].join('\n')
75}
76
77function reportFailure($: EngineInterface, kind: Kind, failure: string, detail: string): string {
78  $.ui.log(`rule-fairy: ${kind} delivery ${failure}`, { to: 'transcript' })
79  return failureEntry(kind, failure, detail)
80}
81
82function hookFailure(error: HookFailure): string {
83  return error.kind === 'timeout' ? 'timed out' : 'threw'
84}
85
86async function deliver(
87  $: EngineInterface,
88  kind: Kind,
89  input: Record<string, unknown>,
90): Promise<readonly string[]> {
91  const root = $.plugin.root
92  const [sessionId, projectDir, cwd, messages] = await Promise.all([
93    $.session.id(),
94    $.session.root(),
95    $.session.cwd(),
96    $.session.messages({ as: 'api' }),
97  ])
98  const ran = await $.process.run([`${root}/hooks/run`, 'claude/mod.bb'], {
99    cwd,
100    env: { CLAUDE_PLUGIN_ROOT: root, CLAUDE_PROJECT_DIR: projectDir },
101    stdin: JSON.stringify({
102      kind,
103      input: { session_id: sessionId, cwd, in_context_frames: frameHeads(messages), ...input },
104    }),
105    timeoutMs: 60_000,
106  })
107  if (ran.exitCode !== 0) return [reportFailure($, kind, `failed (exit ${ran.exitCode})`, ran.stderr)]
108  const printed: { context?: string[] } = JSON.parse(ran.stdout)
109  return printed.context ?? []
110}
111
112function withContext<T extends { context?: readonly string[] }>(
113  carrier: T,
114  context: readonly string[],
115): T {
116  return context.length === 0
117    ? carrier
118    : { ...carrier, context: [...(carrier.context ?? []), ...context] }
119}
120
121// The tool's result with the hook failure's entry attached; a denied call
122// stays as it is.
123function withFailure<R extends { deny?: string; context?: readonly string[] }>(
124  $: EngineInterface,
125  kind: Kind,
126  result: R,
127  error: HookFailure,
128): R {
129  const entry = reportFailure($, kind, hookFailure(error), error.message ?? '')
130  return result.deny !== undefined ? result : withContext(result, [entry])
131}
132
133export const register: Register = on => {
134  on('prompt.submit', async ($, e, next) => {
135    if (COMPOSED_BY_OTHERS.has(e.origin.kind)) return next(e)
136    const context = await deliver($, 'prompt', { prompt: e.text })
137    return next(withContext(e, context))
138  }).catch(($, e, next) => {
139    const entry = reportFailure($, 'prompt', hookFailure(next.error), next.error.message ?? '')
140    return next.called ? next(e) : next(withContext(e, [entry]))
141  })
142
143  on('tool.call', { tool: EDIT_TOOL }, async ($, e, next) => {
144    const result = await next(e)
145    if (e.agentId !== undefined || result.deny !== undefined) return result
146    const { tool, tool_use_id, agentId, ...toolInput } = e
147    const context = await deliver($, 'edit', { tool_name: tool, tool_input: toolInput })
148    return withContext(result, context)
149  }).catch(async ($, e, next) => withFailure($, 'edit', await next(e), next.error))
150
151  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
152    const result = await next(e)
153    if (e.agentId !== undefined || result.deny !== undefined) return result
154    const context = await deliver($, 'shell', {
155      tool_name: 'Bash',
156      tool_input: { command: e.command },
157    })
158    return withContext(result, context)
159  }).catch(async ($, e, next) => withFailure($, 'shell', await next(e), next.error))
160}
161