@webpieces/ai-hook-rules 0.4.735 → 0.4.737

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.
@@ -1 +1 @@
1
- {"version":3,"file":"hook-registration.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/hook-registration.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { toError } from '../core/to-error';\nimport { AiType } from '../core/agent-event';\nimport { SHIM_MARKER, committedShimStale } from './shim';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\n\n/**\n * THE INSTALLED HOOK SURFACE — two hooks per harness, all ABSOLUTE, and the ONE place their spelling is\n * defined. See `HarnessRegistration` below, which is what makes \"per harness\" data rather than four\n * module constants that were only ever true of Claude Code.\n *\n * ─── ONE GOVERNOR: the MAIN tree judges every tree ─────────────────────────────────────────────────\n *\n * Claude Code — .claude/settings.json\n * H1 sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" wp-ai-guards-hook Write|…|Bash|Read\n * H2 sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" wp-ai-rules-hook Write|Edit|MultiEdit\n * Codex — .codex/hooks.json (the SAME `hooks.PreToolUse` JSON shape, measured)\n * H1 sh \"$PWD/.claude/webpieces/ai-hook.sh\" wp-ai-guards-hook Bash|apply_patch\n * H2 sh \"$PWD/.claude/webpieces/ai-hook.sh\" wp-ai-rules-hook apply_patch\n *\n * ONE shim file serves all four: `.claude/` there is a path, not a claim about who is calling.\n *\n * This REPLACES a three-hook form in which these two were RELATIVE (`sh \".claude/webpieces/ai-hook.sh\"`)\n * and a third hook, L-1 `guarantee-root.sh`, existed solely to guarantee that relative path resolved.\n *\n * ─── Why the relative experiment was reversed ──────────────────────────────────────────────────────\n * Relative registration was adopted so each git tree would be governed by \"its own release, binary and\n * pin\". MEASURED 2026-08-10: it never delivered that. A linked worktree has NO `node_modules`, so\n * ai-hook.sh's upward walk executes the PRIMARY's binary — `readlink -f` resolved a worktree agent's bin\n * to `<primary>/node_modules/@webpieces/ai-hook-rules`. A worktree ran its own SCRIPT and its own\n * CONFIG; it never ran its own release. Governance was already the primary's, in every tree, the whole\n * time.\n *\n * The price of that fiction was an entire guard layer. A relative path only resolves at a tree root, and\n * a hook that cannot resolve exits 127 — per the hooks reference a NON-BLOCKING error, i.e. a SILENT\n * UNGUARDED ALLOW. So L-1 had to deny every `cd` into a project subdirectory. That denial produced the\n * force-to-root bug class, the reaped-worktree `cd` prescription, and a measured hard deadlock: L-1 told\n * a worktree-isolated agent to `cd` to the primary clone, which the harness refuses for an isolated\n * agent, leaving it unable to stay or to follow the cure.\n *\n * An absolute path resolves from ANY cwd. So the launch guarantee is structural, L-1 has no job left and\n * is deleted, and `cd` into a subdirectory is simply allowed. What used to be true only by policing the\n * shell is now true by construction.\n *\n * ─── What replaces the property that was lost ──────────────────────────────────────────────────────\n * Nothing is lost that was ever delivered — but the case relative registration WANTED to handle (a tree\n * that genuinely needs a different @webpieces) is now DETECTED instead of silently mis-governed:\n * `VersionSyncGuard` (L1 row 8) blocks when a worktree's pin disagrees with the main tree's, and\n * prescribes either aligning the pins (same git hash → same tracked pin → install in each tree that has\n * a node_modules) or using a separate CLONE, which — unlike a worktree — gets its own GOVERNANCE. Note\n * what that does NOT say: a worktree may perfectly well have its own node_modules, and usually does the\n * moment anything installs in it. What it may not have is a DIFFERENT @webpieces version.\n *\n * ─── Why the registration is a DRIFT SURFACE, not just an install step ─────────────────────────────\n * Nothing used to validate `.claude/settings.json` at all, so a settings file left on a superseded form\n * silently changed who governs. The installed surface is THREE things (ai-hook.sh, the registration, and\n * the managed `env` entry — see managed-env.ts), all three are compared against this release, and\n * `wp-upgrade-shim` regenerates all three. A cure that fixes two of three is worse than no cure, because\n * it reports success.\n */\n\n/**\n * The RETIRED L-1 hook's committed path. Named here, in the one module that must still recognise it, and\n * matched ONLY by isManagedCommand() so repairRegistration() can delete the stale entry. There is no\n * guarantee-root.ts any more — this literal is all that remains of it, deliberately.\n *\n * It is a one-way RECOGNISER, not a shim: nothing emits it, `expectedEntries()` never returns it, and a\n * settings file carrying it is reported STALE. Its only job is to make the retired entry findable so it\n * can be REMOVED.\n */\nexport const LEGACY_GUARANTEE_ROOT_MARKER = '.claude/webpieces/guarantee-root.sh';\n\n/**\n * When this recogniser may be deleted, as a value rather than a comment nobody re-reads.\n *\n * A removal-only migration is still dead weight once no consumer can be carrying the old shape. The\n * hazard of deleting it EARLY is severe and silent — repair would stop stripping the retired entry, and\n * a hook registered against a deleted file exits 127, which the Claude Code hooks reference defines as a\n * NON-BLOCKING error, i.e. every `cd` unjudged while the cure reports success. So it gets a stated date\n * and a test that fails once the date passes, instead of an intention.\n *\n * `legacy-marker-expiry.spec.ts` reads this and fails after it, which is the reminder.\n */\nexport const LEGACY_MARKER_REMOVE_AFTER = '2026-12-01';\n\n/** One PreToolUse hook entry as webpieces registers it. Data-only → a class, per CLAUDE.md. */\nexport class HookRegistrationEntry {\n constructor(\n readonly matcher: string,\n readonly command: string,\n ) {}\n\n sameAs(other: HookRegistrationEntry): boolean {\n return this.matcher === other.matcher && this.command === other.command;\n }\n}\n\n// webpieces-disable no-any-unknown -- settings.json is opaque consumer JSON\nexport interface HookCommand { type: string; command: string; }\nexport interface HookEntry { matcher: string; hooks: HookCommand[]; }\n/**\n * One settings file as webpieces reads it. MEASURED: Codex's `.codex/hooks.json` uses the IDENTICAL\n * `hooks.PreToolUse[].hooks[].command` shape, so one reader, one writer and one repair serve both files\n * — the difference between the harnesses is entirely in the VALUES (matcher, shim anchor), which is what\n * `HarnessRegistration` carries. The `env` block below is Claude Code's alone.\n */\nexport interface ClaudeSettings {\n hooks?: { PreToolUse?: HookEntry[] };\n // Claude Code's settings `env` block: every key is exported into the environment of the session AND\n // of every subagent it spawns. That inheritance is precisely why webpieces pins its managed entry\n // here rather than in a shell profile — see managed-env.ts.\n env?: Record<string, string>;\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport const RULES_BIN = 'wp-ai-rules-hook';\nexport const GUARDS_BIN = 'wp-ai-guards-hook';\n\n/**\n * ONE HARNESS'S registration surface — where its hooks live, what they match, and how they name the\n * shim. Data-only → a class, per CLAUDE.md.\n *\n * ─── Why this exists ──────────────────────────────────────────────────────────────────────────────\n * The matchers and the shim command used to be four module constants written for Claude Code alone.\n * Applied to Codex they are wrong in both halves, and the way they are wrong is SILENT:\n *\n * - MATCHER. Codex's file-editing tool is `apply_patch` (MEASURED, codex-cli 0.151.0). A matcher of\n * `Write|Edit|MultiEdit` matches it never, so every file rule is unreachable while the settings file\n * looks perfectly installed. Its shell tool IS called `Bash` — Codex reuses Claude's name — which is\n * the trap: half the matcher works, so the hooks appear to be running.\n * - ANCHOR. `$CLAUDE_PROJECT_DIR` does not exist in a Codex hook's environment (measured: 46 vars, no\n * such key), so the command expands to `sh \"/.claude/webpieces/ai-hook.sh\"`, which dies — and per the\n * hooks protocol a non-2 non-zero exit is a NON-BLOCKING error, i.e. a silent unguarded allow.\n *\n * Both halves of that were live in real repos, written by a Codex Desktop sync that transliterated the\n * Claude setup. Making the registration per-harness DATA is what stops a future harness inheriting a\n * matcher that was never true for it.\n *\n * ─── ONE SHIM, both harnesses ─────────────────────────────────────────────────────────────────────\n * `SHIM_MARKER` is shared deliberately: `.claude/webpieces/ai-hook.sh` is the single fail-closed entry\n * point, and moving or duplicating it would double the L0 allowlist regexes, the drift surfaces and the\n * cures. The `.claude/` prefix is a path, not a claim about which agent is calling.\n */\nexport class HarnessRegistration {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly aiType: AiType,\n /** How the installer and the drift report name this harness to a human. */\n readonly label: string,\n /** Which tool names the RULES hook (code-style, file-scoped) must see. */\n readonly rulesMatcher: string,\n /**\n * Which tool names the GUARDS hook must see. Wider than the rules matcher by the shell tool and,\n * for Claude Code, by `Read` — Read carries no guard, but the guards hook owns the\n * per-invocation audit log, so matching it records every file the AI opens (log-and-allow fast\n * path in hook-core.ts; a Read is never blocked). Codex has no Read tool at all: a read arrives\n * as `Bash` running a pager, which the shell matcher already covers and read-parity synthesizes.\n */\n readonly guardsMatcher: string,\n /** The settings files this harness's hooks can be installed into, relative to the repo root. */\n readonly settingsFiles: readonly string[],\n /**\n * THE PREFIX THE SHIM PATH IS ANCHORED ON, and the reason each harness needs its own.\n *\n * **BYTE-STABILITY IS A HARD CONSTRAINT ON THE CODEX VALUE.** Codex trusts a hook entry\n * TOFU — `~/.codex/config.toml` records a `trusted_hash` per entry — so ANY change to these bytes\n * invalidates that trust and re-prompts the human, whose third option is `Continue without\n * trusting (hooks won't run)`: one keystroke to a silently unguarded session. Changing this\n * string is therefore never a cosmetic edit. The hash is NOT reproducible from outside Codex\n * (16 encodings tried against a file we authored), so the installer can never repair trust for\n * itself — see ./codex-trust.ts, which REPORTS and never writes.\n */\n readonly shimAnchor: string,\n /**\n * True when this harness's settings file also carries the managed `env` block. Claude Code's\n * settings `env` is inherited by every subagent, which is what makes it the right home for the\n * Bash-cwd pin (see managed-env.ts). Codex has no equivalent surface — and needs none: its cwd\n * is MEASURED not to drift at all (`cd x && pwd` prints x, the next call is back at the repo\n * root, for the coordinator and for subagents alike).\n */\n readonly managesEnv: boolean,\n /** What `managedSurfaceDrift()` calls this harness's registration when it has moved. */\n readonly registrationSurface: string,\n ) {}\n\n /** Which matcher one guard bin registers under. */\n matcherFor(bin: string): string {\n return bin === RULES_BIN ? this.rulesMatcher : this.guardsMatcher;\n }\n\n /**\n * The guard-hook command — `sh \"<anchor>/.claude/webpieces/ai-hook.sh\" <bin>`.\n *\n * ABSOLUTE, in both harnesses. Claude's anchor replaced a RELATIVE spelling and that reversal is\n * documented at length in this file's header: relative resolves only at a tree root, a hook that\n * cannot resolve exits 127, and per the hooks reference that is a NON-BLOCKING error — a silent\n * unguarded allow. It also cost an entire guard layer (L-1) whose only job was to police the `cd`\n * that made the relative path resolvable.\n *\n * Invoked via `sh <file>` rather than executed directly so a missing executable bit on the\n * checked-in shim (fresh clone, a filesystem that drops the bit, git core.fileMode quirks) can never\n * break the hook with a raw `Permission denied` on every tool call. Quoted to survive spaces.\n */\n shimCommand(bin: string): string {\n return `sh \"${this.shimAnchor}/${SHIM_MARKER}\" ${bin}`;\n }\n\n /** The registration entry for one guard bin under this harness. */\n entryFor(bin: string): HookRegistrationEntry {\n return new HookRegistrationEntry(this.matcherFor(bin), this.shimCommand(bin));\n }\n\n /** This harness's settings files under one repo root, absolute. */\n settingsPaths(projectRoot: string): readonly string[] {\n return this.settingsFiles.map((file: string): string => path.join(projectRoot, ...file.split('/')));\n }\n}\n\nexport const CLAUDE_REGISTRATION = new HarnessRegistration(\n 'claude-code', 'Claude Code',\n 'Write|Edit|MultiEdit',\n 'Write|Edit|MultiEdit|Bash|Read',\n ['.claude/settings.json', '.claude/settings.local.json'],\n '$CLAUDE_PROJECT_DIR',\n true,\n '.claude/settings.json hook registration',\n);\n\n/**\n * Codex's registration, every value of it MEASURED against codex-cli 0.151.0 rather than assumed.\n *\n * `$PWD` is the anchor because there is no project-dir variable to use and none is needed: the payload\n * `cwd` and the hook process's own `PWD` are both the repo root on EVERY call — for the coordinator and\n * for subagents — and a `cd` inside one command never survives into the next. That was measured, not\n * hoped for, and it is the same effect Claude Code gets from the managed\n * `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` env entry. See `shimAnchor` for why these exact bytes are\n * not free to change.\n */\nexport const CODEX_REGISTRATION = new HarnessRegistration(\n 'codex', 'Codex',\n 'apply_patch',\n 'Bash|apply_patch',\n ['.codex/hooks.json'],\n '$PWD',\n false,\n '.codex/hooks.json hook registration',\n);\n\n/**\n * Every harness webpieces arms, in installer order.\n *\n * The drift check, the repair and the installer all iterate THIS, so a harness cannot be armed by the\n * installer and then left unvalidated — which is exactly the state `.codex/hooks.json` was in before it\n * was a managed surface: written by something else, silently wrong, and invisible to every check.\n */\nexport const HARNESS_REGISTRATIONS: readonly HarnessRegistration[] = [CLAUDE_REGISTRATION, CODEX_REGISTRATION];\n\n/**\n * True when this PreToolUse command is one webpieces owns — in ANY spelling it has ever shipped.\n *\n * THE SINGLE MOST IMPORTANT LINE IN THIS RELEASE. `LEGACY_GUARANTEE_ROOT_MARKER` is matched here and\n * NOWHERE else: it is how `repairRegistration()` finds and REMOVES the retired H1 entry from a settings\n * file written by an older release. Drop it and the repair silently leaves a live L-1 hook registered\n * against a file this release deletes — exit 127, which the Claude Code hooks reference defines as a\n * NON-BLOCKING error, so every `cd` goes unjudged while `wp-upgrade-shim` reports success and no drift\n * check can name it. Removal-only, never emitted: it appears in no `expectedEntries()` result, so it is\n * a one-way migration and not a second accepted spelling.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function isManagedCommand(command: string): boolean {\n return command.includes(SHIM_MARKER) || command.includes(LEGACY_GUARANTEE_ROOT_MARKER);\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nfunction preToolUse(settings: ClaudeSettings): readonly HookEntry[] {\n return settings.hooks?.PreToolUse ?? [];\n}\n\n/** Every webpieces-managed entry of one settings file, flattened to matcher + command pairs. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function managedEntries(settings: ClaudeSettings): readonly HookRegistrationEntry[] {\n const found: HookRegistrationEntry[] = [];\n for (const entry of preToolUse(settings)) {\n for (const hook of entry.hooks) {\n if (isManagedCommand(hook.command)) found.push(new HookRegistrationEntry(entry.matcher, hook.command));\n }\n }\n return found;\n}\n\n/**\n * Which guard bins this settings file registers, in installer order. A file registering NEITHER is not\n * a project (relative) install and is therefore never judged — a global/absolute install names the bin\n * path directly and carries no shim marker at all.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registeredBins(settings: ClaudeSettings): readonly string[] {\n const commands = managedEntries(settings).map((e: HookRegistrationEntry): string => e.command);\n return [GUARDS_BIN, RULES_BIN].filter((bin: string): boolean => commands.some((c: string): boolean => c.includes(bin)));\n}\n\n/**\n * The exact set of entries THIS RELEASE expects in a settings file that registers `bins`.\n *\n * ONE entry per registered bin, and nothing else. There used to be a third, `guarantee-root.sh`, added\n * whenever the GUARDS bin was present; it is retired, and a settings file still carrying it is STALE —\n * `repairRegistration()` removes it via isManagedCommand()'s legacy marker.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function expectedEntries(harness: HarnessRegistration, bins: readonly string[]): readonly HookRegistrationEntry[] {\n return bins.map((bin: string): HookRegistrationEntry => harness.entryFor(bin));\n}\n\n/**\n * True when a settings file registers webpieces hooks in a shape this release does not expect: a\n * RELATIVE shim command, a leftover guarantee-root entry, a stray duplicate, or a wrong matcher.\n *\n * Compared as a SET, not a sequence: Claude Code runs all matching hooks in parallel, so array order\n * carries no meaning and reordering must not read as drift.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registrationStale(harness: HarnessRegistration, settings: ClaudeSettings): boolean {\n const bins = registeredBins(settings);\n if (bins.length === 0) return false;\n const have = managedEntries(settings);\n const want = expectedEntries(harness, bins);\n if (have.length !== want.length) return true;\n return want.some((w: HookRegistrationEntry): boolean => !have.some((h: HookRegistrationEntry): boolean => h.sameAs(w)));\n}\n\n/** Drop every webpieces-managed PreToolUse command; returns true if anything was removed. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function dropManagedEntries(settings: ClaudeSettings): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand): boolean => !isManagedCommand(h.command));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\n/** Append one PreToolUse entry. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function addHookEntry(settings: ClaudeSettings, entry: HookRegistrationEntry): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher: entry.matcher, hooks: [{ type: 'command', command: entry.command }] });\n}\n\n/** The settings `env` block, or null when the file carries none (or carries junk in its place). */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nfunction settingsEnv(settings: ClaudeSettings): Record<string, string> | null {\n // webpieces-disable no-any-unknown -- settings.json is opaque consumer JSON; `env` is only typed after this check\n const env: unknown = settings.env;\n if (typeof env !== 'object' || env === null || Array.isArray(env)) return null;\n return env as Record<string, string>;\n}\n\n/**\n * True when a settings file that registers webpieces hooks is missing the managed `env` entry, or\n * carries any value other than the one required one.\n *\n * Gated on `registeredBins()` for the SAME reason `registrationStale()` is: a settings file that\n * registers no webpieces hooks is not a project install and is never judged — a global install names\n * the bin path directly and carries no shim marker at all, so there is nothing here to keep in step.\n *\n * NOTE what this entry is for NOW. It was originally justified by keeping the then-RELATIVE hook path\n * resolvable; that job is retired, because both hooks are absolute and resolve from any cwd. It is kept\n * for VERDICT STABILITY — a guard's answer must depend on the command, not on where an earlier `cd` left\n * the shell — and because settings `env` is inherited, every subagent shares that cwd and therefore that\n * verdict. See managed-env.ts, which states this at length.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function envStale(settings: ClaudeSettings): boolean {\n if (registeredBins(settings).length === 0) return false;\n const env = settingsEnv(settings);\n return env === null || env[BASH_CWD_ENV_KEY] !== BASH_CWD_ENV_VALUE;\n}\n\n/**\n * Set the managed `env` entry IN PLACE, whatever was there before. Returns true when it changed.\n *\n * A user-set `\"0\"` is BROUGHT TO `\"1\"`, not honoured: this is a managed surface, and \"webpieces sets it\n * unless you disagreed\" would be a second, invisible spelling of the decision. Turning it off means\n * uninstalling the hooks, exactly as it does for every other managed thing.\n *\n * UNGATED on purpose — the installer calls it for the file it is writing hooks into, and the gate lives\n * at the caller (repairRegistration / applyHook), so a file with no webpieces hooks is never touched.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function applyManagedEnv(settings: ClaudeSettings): boolean {\n const existing = settingsEnv(settings);\n if (existing !== null && existing[BASH_CWD_ENV_KEY] === BASH_CWD_ENV_VALUE) return false;\n const env = existing ?? {};\n env[BASH_CWD_ENV_KEY] = BASH_CWD_ENV_VALUE;\n settings.env = env;\n return true;\n}\n\n/**\n * Bring one settings object to the two-hook ABSOLUTE form AND the managed `env` entry IN PLACE. Returns\n * true when it changed anything.\n *\n * REMOVE-then-ADD for the hooks, never add-beside: two spellings of one registration is exactly the\n * compatibility shim the backwards-compat reviewer rejects, and leaving a RELATIVE entry beside the\n * absolute one would run two shims per call — the\n * straddle this whole change exists to delete.\n *\n * THE TWO REPAIRS ARE EVALUATED INDEPENDENTLY, and that is load-bearing rather than tidy: this used to\n * early-return on `!registrationStale(settings)`, so a repo whose hooks are already current but whose\n * `env` entry is missing — the state EVERY existing consumer is in the moment this release lands — would\n * have been reported as drifted by fault S and then left unrepaired by its own prescribed cure. A cure\n * that skips half the surface is the failure mode `upgrade-shim.ts`'s header exists to prevent.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function repairRegistration(harness: HarnessRegistration, settings: ClaudeSettings): boolean {\n const bins = registeredBins(settings);\n if (bins.length === 0) return false;\n let changed = false;\n if (registrationStale(harness, settings)) {\n dropManagedEntries(settings);\n for (const entry of expectedEntries(harness, bins)) addHookEntry(settings, entry);\n changed = true;\n }\n // Only where the harness HAS that surface. Codex's hooks.json has no `env` block to manage, and\n // inventing one there would write a key Codex does not read — a managed surface nothing consumes is\n // a surface that can drift with no consequence and no way to notice.\n if (harness.managesEnv && applyManagedEnv(settings)) changed = true;\n return changed;\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\n/**\n * True when any settings file ONE HARNESS owns under `root` carries a stale registration.\n *\n * Per harness, not per repo, so the drift report can NAME which one moved — `.claude/settings.json` and\n * `.codex/hooks.json` have different cures, and a fault that says only \"the registration is stale\" sends\n * the reader to the wrong file half the time.\n *\n * A file that does not exist, or exists and registers no webpieces hooks, is never judged (see\n * `registrationStale`). That is what keeps a repo which has never armed Codex from suddenly faulting on\n * a `.codex/hooks.json` it does not have.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function registrationStaleAt(harness: HarnessRegistration, root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return harness.settingsPaths(root).some((p: string): boolean => registrationStale(harness, readSettings(p)));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable/invalid settings counts as \"not stale\" so it never wedges a call\n return false;\n }\n}\n\n/**\n * True when either Claude project settings file under `root` is missing the managed `env` entry.\n *\n * CLAUDE-ONLY by construction, and stated as such rather than looped over the harnesses: `env` is a\n * Claude Code settings surface, Codex has no equivalent, and Codex's cwd is measured not to drift, which\n * is the whole thing the entry is for. See HarnessRegistration.managesEnv.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function envStaleAt(root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return CLAUDE_REGISTRATION.settingsPaths(root).some((p: string): boolean => envStale(readSettings(p)));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable/invalid settings counts as \"not stale\" so it never wedges a call\n return false;\n }\n}\n\n/**\n * WHAT was rewritten in ONE settings file. Data-only → a class, per CLAUDE.md.\n *\n * Two independent flags rather than one path, because the cure has to be able to SAY which repair it\n * made: \"rewrote the hook registration\" printed for a file whose registration was already current and\n * whose `env` entry was the only thing missing is a cure lying about its own work.\n */\nexport class SettingsRepair {\n constructor(\n readonly settingsPath: string,\n readonly registration: boolean,\n readonly env: boolean,\n ) {}\n}\n\n/**\n * Rewrite every stale settings file under `root`, for EVERY harness; returns what changed, per file.\n *\n * Existing files only — this never CREATES a registration. Arming a harness is the installer's decision\n * (`wp-install-ai-hooks`); this is the cure for one that has already been armed and has drifted, so a\n * repo that has never armed Codex is left exactly as it was.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function repairRegistrationAt(root: string): readonly SettingsRepair[] {\n const repairs: SettingsRepair[] = [];\n for (const harness of HARNESS_REGISTRATIONS) {\n for (const settingsPath of harness.settingsPaths(root)) {\n if (!fs.existsSync(settingsPath)) continue;\n const settings = readSettings(settingsPath);\n const neededRegistration = registrationStale(harness, settings);\n const neededEnv = harness.managesEnv && envStale(settings);\n if (!repairRegistration(harness, settings)) continue;\n writeSettings(settingsPath, settings);\n repairs.push(new SettingsRepair(settingsPath, neededRegistration, neededEnv));\n }\n }\n return repairs;\n}\n\n/**\n * The names the drift check reports, so a deny can say WHICH surface moved.\n *\n * There were four, then three, and there are now FOUR again — but the fourth is not the one that was\n * deleted. `GUARANTEE_ROOT_SURFACE` went with L-1 itself: an absolutely-registered shim resolves from\n * any cwd, so there is no launch guarantee left to police and no second .sh file to keep byte-locked. A\n * settings file still carrying the retired H1 entry is not its own surface — it is ordinary REGISTRATION\n * drift, which `registrationStaleAt()` reports and `repairRegistration()` fixes by removing it.\n *\n * The new fourth is `.codex/hooks.json`, and it is here because of what happened while it was NOT a\n * managed surface: something else wrote it, with a matcher that matched no Codex file tool and a shim\n * path anchored on a variable Codex does not set, and no check in this package could see it. A file the\n * guards depend on and nothing validates is the exact shape of that incident.\n *\n * Each harness's registration surface is its own name, from HarnessRegistration.registrationSurface, so\n * the deny sends the reader to the file that actually moved.\n */\nexport const SHIM_SURFACE = SHIM_MARKER;\nexport const REGISTRATION_SURFACE = CLAUDE_REGISTRATION.registrationSurface;\nexport const CODEX_REGISTRATION_SURFACE = CODEX_REGISTRATION.registrationSurface;\nexport const ENV_SURFACE = `.claude/settings.json env.${BASH_CWD_ENV_KEY}`;\n\n/**\n * WHICH of the managed surfaces disagree with this release — the input to fault S.\n *\n * All of them are checked against the SAME root, resolved from the RUNNING MODULE (governingShimRoot),\n * never from cwd and never from `$CLAUDE_PROJECT_DIR`: the files we compare and the renderers we\n * compare them TO must come from one install, or the check straddles two trees and can never converge.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function managedSurfaceDrift(root: string | null): readonly string[] {\n const drifted: string[] = [];\n if (committedShimStale(root)) drifted.push(SHIM_SURFACE);\n for (const harness of HARNESS_REGISTRATIONS) {\n if (registrationStaleAt(harness, root)) drifted.push(harness.registrationSurface);\n }\n if (envStaleAt(root)) drifted.push(ENV_SURFACE);\n return drifted;\n}\n"]}
1
+ {"version":3,"file":"hook-registration.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/hook-registration.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { toError } from '../core/to-error';\nimport { AiType } from '../core/agent-event';\nimport { SHIM_MARKER, committedShimStale } from './shim';\nimport { NEIGHBOUR_SURFACE_SUFFIX, anchorNeighbourHooks, neighbourHooksStale } from './neighbour-hooks';\nimport type { ClaudeSettings, HookCommand, HookEntry } from './settings-shape';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\n\n/**\n * THE INSTALLED HOOK SURFACE — two hooks per harness, all ABSOLUTE, and the ONE place their spelling is\n * defined. See `HarnessRegistration` below, which is what makes \"per harness\" data rather than four\n * module constants that were only ever true of Claude Code.\n *\n * ─── ONE GOVERNOR: the MAIN tree judges every tree ─────────────────────────────────────────────────\n *\n * Claude Code — .claude/settings.json\n * H1 sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" wp-ai-guards-hook Write|…|Bash|Read\n * H2 sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" wp-ai-rules-hook Write|Edit|MultiEdit\n * Codex — .codex/hooks.json (the SAME `hooks.PreToolUse` JSON shape, measured)\n * H1 sh \"$PWD/.claude/webpieces/ai-hook.sh\" wp-ai-guards-hook Bash|apply_patch\n * H2 sh \"$PWD/.claude/webpieces/ai-hook.sh\" wp-ai-rules-hook apply_patch\n *\n * ONE shim file serves all four: `.claude/` there is a path, not a claim about who is calling.\n *\n * This REPLACES a three-hook form in which these two were RELATIVE (`sh \".claude/webpieces/ai-hook.sh\"`)\n * and a third hook, L-1 `guarantee-root.sh`, existed solely to guarantee that relative path resolved.\n *\n * ─── Why the relative experiment was reversed ──────────────────────────────────────────────────────\n * Relative registration was adopted so each git tree would be governed by \"its own release, binary and\n * pin\". MEASURED 2026-08-10: it never delivered that. A linked worktree has NO `node_modules`, so\n * ai-hook.sh's upward walk executes the PRIMARY's binary — `readlink -f` resolved a worktree agent's bin\n * to `<primary>/node_modules/@webpieces/ai-hook-rules`. A worktree ran its own SCRIPT and its own\n * CONFIG; it never ran its own release. Governance was already the primary's, in every tree, the whole\n * time.\n *\n * The price of that fiction was an entire guard layer. A relative path only resolves at a tree root, and\n * a hook that cannot resolve exits 127 — per the hooks reference a NON-BLOCKING error, i.e. a SILENT\n * UNGUARDED ALLOW. So L-1 had to deny every `cd` into a project subdirectory. That denial produced the\n * force-to-root bug class, the reaped-worktree `cd` prescription, and a measured hard deadlock: L-1 told\n * a worktree-isolated agent to `cd` to the primary clone, which the harness refuses for an isolated\n * agent, leaving it unable to stay or to follow the cure.\n *\n * An absolute path resolves from ANY cwd. So the launch guarantee is structural, L-1 has no job left and\n * is deleted, and `cd` into a subdirectory is simply allowed. What used to be true only by policing the\n * shell is now true by construction.\n *\n * ─── What replaces the property that was lost ──────────────────────────────────────────────────────\n * Nothing is lost that was ever delivered — but the case relative registration WANTED to handle (a tree\n * that genuinely needs a different @webpieces) is now DETECTED instead of silently mis-governed:\n * `VersionSyncGuard` (L1 row 8) blocks when a worktree's pin disagrees with the main tree's, and\n * prescribes either aligning the pins (same git hash → same tracked pin → install in each tree that has\n * a node_modules) or using a separate CLONE, which — unlike a worktree — gets its own GOVERNANCE. Note\n * what that does NOT say: a worktree may perfectly well have its own node_modules, and usually does the\n * moment anything installs in it. What it may not have is a DIFFERENT @webpieces version.\n *\n * ─── Why the registration is a DRIFT SURFACE, not just an install step ─────────────────────────────\n * Nothing used to validate `.claude/settings.json` at all, so a settings file left on a superseded form\n * silently changed who governs. The installed surface is FOUR things (ai-hook.sh, the registration, the\n * managed `env` entry — see managed-env.ts — and the ANCHORING of the NEIGHBOUR hook commands the\n * consumer registers beside ours, see neighbour-hooks.ts), all four are compared against this release,\n * and `wp-upgrade-shim` repairs all four. A cure that fixes three of four is worse than no cure, because\n * it reports success.\n */\n\n/**\n * The RETIRED L-1 hook's committed path. Named here, in the one module that must still recognise it, and\n * matched ONLY by isManagedCommand() so repairRegistration() can delete the stale entry. There is no\n * guarantee-root.ts any more — this literal is all that remains of it, deliberately.\n *\n * It is a one-way RECOGNISER, not a shim: nothing emits it, `expectedEntries()` never returns it, and a\n * settings file carrying it is reported STALE. Its only job is to make the retired entry findable so it\n * can be REMOVED.\n */\nexport const LEGACY_GUARANTEE_ROOT_MARKER = '.claude/webpieces/guarantee-root.sh';\n\n/**\n * When this recogniser may be deleted, as a value rather than a comment nobody re-reads.\n *\n * A removal-only migration is still dead weight once no consumer can be carrying the old shape. The\n * hazard of deleting it EARLY is severe and silent — repair would stop stripping the retired entry, and\n * a hook registered against a deleted file exits 127, which the Claude Code hooks reference defines as a\n * NON-BLOCKING error, i.e. every `cd` unjudged while the cure reports success. So it gets a stated date\n * and a test that fails once the date passes, instead of an intention.\n *\n * `legacy-marker-expiry.spec.ts` reads this and fails after it, which is the reminder.\n */\nexport const LEGACY_MARKER_REMOVE_AFTER = '2026-12-01';\n\n/** One PreToolUse hook entry as webpieces registers it. Data-only → a class, per CLAUDE.md. */\nexport class HookRegistrationEntry {\n constructor(\n readonly matcher: string,\n readonly command: string,\n ) {}\n\n sameAs(other: HookRegistrationEntry): boolean {\n return this.matcher === other.matcher && this.command === other.command;\n }\n}\n\n/**\n * THE SETTINGS-FILE SHAPE lives in `./settings-shape`, a LEAF module, and is imported from THERE by\n * everything that needs it — this module included. It is deliberately NOT re-exported here: a second\n * import path for one type is the \"two spellings of one thing\" shim the compatibility policy rejects,\n * and it would keep advertising `hook-registration` as a source of the shape, which is precisely the\n * edge that has to stay broken (`neighbour-hooks.ts` needs the shape, this module imports the repairs\n * FROM it, and `validate-no-file-import-cycles` counts even a type-only edge back as a cycle).\n */\n\nexport const RULES_BIN = 'wp-ai-rules-hook';\nexport const GUARDS_BIN = 'wp-ai-guards-hook';\n\n/**\n * ONE HARNESS'S registration surface — where its hooks live, what they match, and how they name the\n * shim. Data-only → a class, per CLAUDE.md.\n *\n * ─── Why this exists ──────────────────────────────────────────────────────────────────────────────\n * The matchers and the shim command used to be four module constants written for Claude Code alone.\n * Applied to Codex they are wrong in both halves, and the way they are wrong is SILENT:\n *\n * - MATCHER. Codex's file-editing tool is `apply_patch` (MEASURED, codex-cli 0.151.0). A matcher of\n * `Write|Edit|MultiEdit` matches it never, so every file rule is unreachable while the settings file\n * looks perfectly installed. Its shell tool IS called `Bash` — Codex reuses Claude's name — which is\n * the trap: half the matcher works, so the hooks appear to be running.\n * - ANCHOR. `$CLAUDE_PROJECT_DIR` does not exist in a Codex hook's environment (measured: 46 vars, no\n * such key), so the command expands to `sh \"/.claude/webpieces/ai-hook.sh\"`, which dies — and per the\n * hooks protocol a non-2 non-zero exit is a NON-BLOCKING error, i.e. a silent unguarded allow.\n *\n * Both halves of that were live in real repos, written by a Codex Desktop sync that transliterated the\n * Claude setup. Making the registration per-harness DATA is what stops a future harness inheriting a\n * matcher that was never true for it.\n *\n * ─── ONE SHIM, both harnesses ─────────────────────────────────────────────────────────────────────\n * `SHIM_MARKER` is shared deliberately: `.claude/webpieces/ai-hook.sh` is the single fail-closed entry\n * point, and moving or duplicating it would double the L0 allowlist regexes, the drift surfaces and the\n * cures. The `.claude/` prefix is a path, not a claim about which agent is calling.\n */\nexport class HarnessRegistration {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly aiType: AiType,\n /** How the installer and the drift report name this harness to a human. */\n readonly label: string,\n /** Which tool names the RULES hook (code-style, file-scoped) must see. */\n readonly rulesMatcher: string,\n /**\n * Which tool names the GUARDS hook must see. Wider than the rules matcher by the shell tool and,\n * for Claude Code, by `Read` — Read carries no guard, but the guards hook owns the\n * per-invocation audit log, so matching it records every file the AI opens (log-and-allow fast\n * path in hook-core.ts; a Read is never blocked). Codex has no Read tool at all: a read arrives\n * as `Bash` running a pager, which the shell matcher already covers and read-parity synthesizes.\n */\n readonly guardsMatcher: string,\n /** The settings files this harness's hooks can be installed into, relative to the repo root. */\n readonly settingsFiles: readonly string[],\n /**\n * THE PREFIX THE SHIM PATH IS ANCHORED ON, and the reason each harness needs its own.\n *\n * **BYTE-STABILITY IS A HARD CONSTRAINT ON THE CODEX VALUE.** Codex trusts a hook entry\n * TOFU — `~/.codex/config.toml` records a `trusted_hash` per entry — so ANY change to these bytes\n * invalidates that trust and re-prompts the human, whose third option is `Continue without\n * trusting (hooks won't run)`: one keystroke to a silently unguarded session. Changing this\n * string is therefore never a cosmetic edit. The hash is NOT reproducible from outside Codex\n * (16 encodings tried against a file we authored), so the installer can never repair trust for\n * itself — see ./codex-trust.ts, which REPORTS and never writes.\n */\n readonly shimAnchor: string,\n /**\n * True when this harness's settings file also carries the managed `env` block. Claude Code's\n * settings `env` is inherited by every subagent, which is what makes it the right home for the\n * Bash-cwd pin (see managed-env.ts). Codex has no equivalent surface — and needs none: its cwd\n * is MEASURED not to drift at all (`cd x && pwd` prints x, the next call is back at the repo\n * root, for the coordinator and for subagents alike).\n */\n readonly managesEnv: boolean,\n /** What `managedSurfaceDrift()` calls this harness's registration when it has moved. */\n readonly registrationSurface: string,\n ) {}\n\n /** Which matcher one guard bin registers under. */\n matcherFor(bin: string): string {\n return bin === RULES_BIN ? this.rulesMatcher : this.guardsMatcher;\n }\n\n /**\n * The guard-hook command — `sh \"<anchor>/.claude/webpieces/ai-hook.sh\" <bin>`.\n *\n * ABSOLUTE, in both harnesses. Claude's anchor replaced a RELATIVE spelling and that reversal is\n * documented at length in this file's header: relative resolves only at a tree root, a hook that\n * cannot resolve exits 127, and per the hooks reference that is a NON-BLOCKING error — a silent\n * unguarded allow. It also cost an entire guard layer (L-1) whose only job was to police the `cd`\n * that made the relative path resolvable.\n *\n * Invoked via `sh <file>` rather than executed directly so a missing executable bit on the\n * checked-in shim (fresh clone, a filesystem that drops the bit, git core.fileMode quirks) can never\n * break the hook with a raw `Permission denied` on every tool call. Quoted to survive spaces.\n */\n shimCommand(bin: string): string {\n return `sh \"${this.shimAnchor}/${SHIM_MARKER}\" ${bin}`;\n }\n\n /** The registration entry for one guard bin under this harness. */\n entryFor(bin: string): HookRegistrationEntry {\n return new HookRegistrationEntry(this.matcherFor(bin), this.shimCommand(bin));\n }\n\n /** This harness's settings files under one repo root, absolute. */\n settingsPaths(projectRoot: string): readonly string[] {\n return this.settingsFiles.map((file: string): string => path.join(projectRoot, ...file.split('/')));\n }\n\n /**\n * What `managedSurfaceDrift()` calls this harness's NEIGHBOUR hook commands — the entries a CONSUMER\n * repo registers in the same file — when one of them still carries a repo-RELATIVE entry path.\n *\n * DERIVED rather than a constructor field, unlike `registrationSurface` beside it, because there is\n * nothing here a harness could sensibly disagree about: it is the same file, said a second way. A\n * constructor param would be a second place to keep in step for no decision.\n */\n get neighbourSurface(): string {\n return `${this.settingsFiles[0]} ${NEIGHBOUR_SURFACE_SUFFIX}`;\n }\n}\n\nexport const CLAUDE_REGISTRATION = new HarnessRegistration(\n 'claude-code', 'Claude Code',\n 'Write|Edit|MultiEdit',\n 'Write|Edit|MultiEdit|Bash|Read',\n ['.claude/settings.json', '.claude/settings.local.json'],\n '$CLAUDE_PROJECT_DIR',\n true,\n '.claude/settings.json hook registration',\n);\n\n/**\n * Codex's registration, every value of it MEASURED against codex-cli 0.151.0 rather than assumed.\n *\n * `$PWD` is the anchor because there is no project-dir variable to use and none is needed: the payload\n * `cwd` and the hook process's own `PWD` are both the repo root on EVERY call — for the coordinator and\n * for subagents — and a `cd` inside one command never survives into the next. That was measured, not\n * hoped for, and it is the same effect Claude Code gets from the managed\n * `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` env entry. See `shimAnchor` for why these exact bytes are\n * not free to change.\n */\nexport const CODEX_REGISTRATION = new HarnessRegistration(\n 'codex', 'Codex',\n 'apply_patch',\n 'Bash|apply_patch',\n ['.codex/hooks.json'],\n '$PWD',\n false,\n '.codex/hooks.json hook registration',\n);\n\n/**\n * Every harness webpieces arms, in installer order.\n *\n * The drift check, the repair and the installer all iterate THIS, so a harness cannot be armed by the\n * installer and then left unvalidated — which is exactly the state `.codex/hooks.json` was in before it\n * was a managed surface: written by something else, silently wrong, and invisible to every check.\n */\nexport const HARNESS_REGISTRATIONS: readonly HarnessRegistration[] = [CLAUDE_REGISTRATION, CODEX_REGISTRATION];\n\n/**\n * True when this PreToolUse command is one webpieces owns — in ANY spelling it has ever shipped.\n *\n * THE SINGLE MOST IMPORTANT LINE IN THIS RELEASE. `LEGACY_GUARANTEE_ROOT_MARKER` is matched here and\n * NOWHERE else: it is how `repairRegistration()` finds and REMOVES the retired H1 entry from a settings\n * file written by an older release. Drop it and the repair silently leaves a live L-1 hook registered\n * against a file this release deletes — exit 127, which the Claude Code hooks reference defines as a\n * NON-BLOCKING error, so every `cd` goes unjudged while `wp-upgrade-shim` reports success and no drift\n * check can name it. Removal-only, never emitted: it appears in no `expectedEntries()` result, so it is\n * a one-way migration and not a second accepted spelling.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function isManagedCommand(command: string): boolean {\n return command.includes(SHIM_MARKER) || command.includes(LEGACY_GUARANTEE_ROOT_MARKER);\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nfunction preToolUse(settings: ClaudeSettings): readonly HookEntry[] {\n return settings.hooks?.PreToolUse ?? [];\n}\n\n/** Every webpieces-managed entry of one settings file, flattened to matcher + command pairs. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function managedEntries(settings: ClaudeSettings): readonly HookRegistrationEntry[] {\n const found: HookRegistrationEntry[] = [];\n for (const entry of preToolUse(settings)) {\n for (const hook of entry.hooks) {\n if (isManagedCommand(hook.command)) found.push(new HookRegistrationEntry(entry.matcher, hook.command));\n }\n }\n return found;\n}\n\n/**\n * Which guard bins this settings file registers, in installer order. A file registering NEITHER is not\n * a project (relative) install and is therefore never judged — a global/absolute install names the bin\n * path directly and carries no shim marker at all.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registeredBins(settings: ClaudeSettings): readonly string[] {\n const commands = managedEntries(settings).map((e: HookRegistrationEntry): string => e.command);\n return [GUARDS_BIN, RULES_BIN].filter((bin: string): boolean => commands.some((c: string): boolean => c.includes(bin)));\n}\n\n/**\n * The exact set of entries THIS RELEASE expects in a settings file that registers `bins`.\n *\n * ONE entry per registered bin, and nothing else. There used to be a third, `guarantee-root.sh`, added\n * whenever the GUARDS bin was present; it is retired, and a settings file still carrying it is STALE —\n * `repairRegistration()` removes it via isManagedCommand()'s legacy marker.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function expectedEntries(harness: HarnessRegistration, bins: readonly string[]): readonly HookRegistrationEntry[] {\n return bins.map((bin: string): HookRegistrationEntry => harness.entryFor(bin));\n}\n\n/**\n * True when a settings file registers webpieces hooks in a shape this release does not expect: a\n * RELATIVE shim command, a leftover guarantee-root entry, a stray duplicate, or a wrong matcher.\n *\n * Compared as a SET, not a sequence: Claude Code runs all matching hooks in parallel, so array order\n * carries no meaning and reordering must not read as drift.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function registrationStale(harness: HarnessRegistration, settings: ClaudeSettings): boolean {\n const bins = registeredBins(settings);\n if (bins.length === 0) return false;\n const have = managedEntries(settings);\n const want = expectedEntries(harness, bins);\n if (have.length !== want.length) return true;\n return want.some((w: HookRegistrationEntry): boolean => !have.some((h: HookRegistrationEntry): boolean => h.sameAs(w)));\n}\n\n/** Drop every webpieces-managed PreToolUse command; returns true if anything was removed. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function dropManagedEntries(settings: ClaudeSettings): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand): boolean => !isManagedCommand(h.command));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\n/** Append one PreToolUse entry. */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function addHookEntry(settings: ClaudeSettings, entry: HookRegistrationEntry): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher: entry.matcher, hooks: [{ type: 'command', command: entry.command }] });\n}\n\n/** The settings `env` block, or null when the file carries none (or carries junk in its place). */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nfunction settingsEnv(settings: ClaudeSettings): Record<string, string> | null {\n // webpieces-disable no-any-unknown -- settings.json is opaque consumer JSON; `env` is only typed after this check\n const env: unknown = settings.env;\n if (typeof env !== 'object' || env === null || Array.isArray(env)) return null;\n return env as Record<string, string>;\n}\n\n/**\n * True when a settings file that registers webpieces hooks is missing the managed `env` entry, or\n * carries any value other than the one required one.\n *\n * Gated on `registeredBins()` for the SAME reason `registrationStale()` is: a settings file that\n * registers no webpieces hooks is not a project install and is never judged — a global install names\n * the bin path directly and carries no shim marker at all, so there is nothing here to keep in step.\n *\n * NOTE what this entry is for NOW. It was originally justified by keeping the then-RELATIVE hook path\n * resolvable; that job is retired, because both hooks are absolute and resolve from any cwd. It is kept\n * for VERDICT STABILITY — a guard's answer must depend on the command, not on where an earlier `cd` left\n * the shell — and because settings `env` is inherited, every subagent shares that cwd and therefore that\n * verdict. See managed-env.ts, which states this at length.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function envStale(settings: ClaudeSettings): boolean {\n if (registeredBins(settings).length === 0) return false;\n const env = settingsEnv(settings);\n return env === null || env[BASH_CWD_ENV_KEY] !== BASH_CWD_ENV_VALUE;\n}\n\n/**\n * Set the managed `env` entry IN PLACE, whatever was there before. Returns true when it changed.\n *\n * A user-set `\"0\"` is BROUGHT TO `\"1\"`, not honoured: this is a managed surface, and \"webpieces sets it\n * unless you disagreed\" would be a second, invisible spelling of the decision. Turning it off means\n * uninstalling the hooks, exactly as it does for every other managed thing.\n *\n * UNGATED on purpose — the installer calls it for the file it is writing hooks into, and the gate lives\n * at the caller (repairRegistration / applyHook), so a file with no webpieces hooks is never touched.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function applyManagedEnv(settings: ClaudeSettings): boolean {\n const existing = settingsEnv(settings);\n if (existing !== null && existing[BASH_CWD_ENV_KEY] === BASH_CWD_ENV_VALUE) return false;\n const env = existing ?? {};\n env[BASH_CWD_ENV_KEY] = BASH_CWD_ENV_VALUE;\n settings.env = env;\n return true;\n}\n\n/**\n * Bring one settings object to the two-hook ABSOLUTE form AND the managed `env` entry IN PLACE. Returns\n * true when it changed anything.\n *\n * REMOVE-then-ADD for the hooks, never add-beside: two spellings of one registration is exactly the\n * compatibility shim the backwards-compat reviewer rejects, and leaving a RELATIVE entry beside the\n * absolute one would run two shims per call — the\n * straddle this whole change exists to delete.\n *\n * THE TWO REPAIRS ARE EVALUATED INDEPENDENTLY, and that is load-bearing rather than tidy: this used to\n * early-return on `!registrationStale(settings)`, so a repo whose hooks are already current but whose\n * `env` entry is missing — the state EVERY existing consumer is in the moment this release lands — would\n * have been reported as drifted by fault S and then left unrepaired by its own prescribed cure. A cure\n * that skips half the surface is the failure mode `upgrade-shim.ts`'s header exists to prevent.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function repairRegistration(harness: HarnessRegistration, settings: ClaudeSettings): boolean {\n const bins = registeredBins(settings);\n if (bins.length === 0) return false;\n let changed = false;\n if (registrationStale(harness, settings)) {\n dropManagedEntries(settings);\n for (const entry of expectedEntries(harness, bins)) addHookEntry(settings, entry);\n changed = true;\n }\n // Only where the harness HAS that surface. Codex's hooks.json has no `env` block to manage, and\n // inventing one there would write a key Codex does not read — a managed surface nothing consumes is\n // a surface that can drift with no consequence and no way to notice.\n if (harness.managesEnv && applyManagedEnv(settings)) changed = true;\n return changed;\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as shimCommand above\nexport function writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\n/**\n * True when any settings file ONE HARNESS owns under `root` carries a NEIGHBOUR hook — one the consumer\n * repo wrote — whose entry path is repo-RELATIVE and would therefore fail to resolve from any cwd but\n * the root. See neighbour-hooks.ts for the measured failure and why webpieces owns the repair.\n *\n * Judged ONLY where the file registers webpieces hooks, exactly like `registrationStaleAt` below: a\n * settings file with no webpieces hooks in it is not a webpieces install, and webpieces rewriting\n * somebody's unrelated hook lines there would be editing a file it was never given.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function neighbourHooksStaleAt(harness: HarnessRegistration, root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return harness.settingsPaths(root).some((p: string): boolean => {\n const settings = readSettings(p);\n return registeredBins(settings).length > 0 && neighbourHooksStale(harness.shimAnchor, settings, root);\n });\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable/invalid settings counts as \"not stale\" so it never wedges a call\n return false;\n }\n}\n\n/**\n * True when any settings file ONE HARNESS owns under `root` carries a stale registration.\n *\n * Per harness, not per repo, so the drift report can NAME which one moved — `.claude/settings.json` and\n * `.codex/hooks.json` have different cures, and a fault that says only \"the registration is stale\" sends\n * the reader to the wrong file half the time.\n *\n * A file that does not exist, or exists and registers no webpieces hooks, is never judged (see\n * `registrationStale`). That is what keeps a repo which has never armed Codex from suddenly faulting on\n * a `.codex/hooks.json` it does not have.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function registrationStaleAt(harness: HarnessRegistration, root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return harness.settingsPaths(root).some((p: string): boolean => registrationStale(harness, readSettings(p)));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable/invalid settings counts as \"not stale\" so it never wedges a call\n return false;\n }\n}\n\n/**\n * True when either Claude project settings file under `root` is missing the managed `env` entry.\n *\n * CLAUDE-ONLY by construction, and stated as such rather than looped over the harnesses: `env` is a\n * Claude Code settings surface, Codex has no equivalent, and Codex's cwd is measured not to drift, which\n * is the whole thing the entry is for. See HarnessRegistration.managesEnv.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function envStaleAt(root: string | null): boolean {\n if (root === null) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return CLAUDE_REGISTRATION.settingsPaths(root).some((p: string): boolean => envStale(readSettings(p)));\n } catch (err: unknown) {\n const error = toError(err);\n void error; // best-effort: unreadable/invalid settings counts as \"not stale\" so it never wedges a call\n return false;\n }\n}\n\n/**\n * WHAT was rewritten in ONE settings file. Data-only → a class, per CLAUDE.md.\n *\n * Two independent flags rather than one path, because the cure has to be able to SAY which repair it\n * made: \"rewrote the hook registration\" printed for a file whose registration was already current and\n * whose `env` entry was the only thing missing is a cure lying about its own work.\n */\nexport class SettingsRepair {\n constructor(\n readonly settingsPath: string,\n readonly registration: boolean,\n readonly env: boolean,\n /**\n * The NEW spelling of every NEIGHBOUR hook command this repair anchored — the consumer's own\n * entries, which webpieces rewrites but does not author.\n *\n * The commands themselves rather than a count or a flag, because this is the one repair that\n * edits lines webpieces did not write: the cure has to be able to show the consumer exactly what\n * it changed in their file, or it is a silent edit to somebody else's hooks.\n */\n readonly anchoredNeighbours: readonly string[],\n ) {}\n}\n\n/**\n * Rewrite every stale settings file under `root`, for EVERY harness; returns what changed, per file.\n *\n * Existing files only — this never CREATES a registration. Arming a harness is the installer's decision\n * (`wp-install-ai-hooks`); this is the cure for one that has already been armed and has drifted, so a\n * repo that has never armed Codex is left exactly as it was.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function repairRegistrationAt(root: string): readonly SettingsRepair[] {\n const repairs: SettingsRepair[] = [];\n for (const harness of HARNESS_REGISTRATIONS) {\n for (const settingsPath of harness.settingsPaths(root)) {\n if (!fs.existsSync(settingsPath)) continue;\n const settings = readSettings(settingsPath);\n const neededRegistration = registrationStale(harness, settings);\n const neededEnv = harness.managesEnv && envStale(settings);\n const changedManaged = repairRegistration(harness, settings);\n // AFTER the managed repair, never before: repairRegistration() removes the retired\n // guarantee-root entry and rewrites the two managed commands, so by the time the neighbour\n // pass runs there is nothing webpieces-owned left for it to look at. Gated on\n // registeredBins() for the same reason every other judgement here is — a settings file that\n // registers no webpieces hooks is not a webpieces install, and rewriting somebody's\n // unrelated hook lines in it would be webpieces editing a file it was never given.\n const anchored = registeredBins(settings).length === 0 ? [] : anchorNeighbourHooks(harness.shimAnchor, settings, root);\n if (!changedManaged && anchored.length === 0) continue;\n writeSettings(settingsPath, settings);\n repairs.push(new SettingsRepair(settingsPath, neededRegistration, neededEnv, anchored));\n }\n }\n return repairs;\n}\n\n/**\n * The names the drift check reports, so a deny can say WHICH surface moved.\n *\n * There were four, then three, and there are now FOUR again — but the fourth is not the one that was\n * deleted. `GUARANTEE_ROOT_SURFACE` went with L-1 itself: an absolutely-registered shim resolves from\n * any cwd, so there is no launch guarantee left to police and no second .sh file to keep byte-locked. A\n * settings file still carrying the retired H1 entry is not its own surface — it is ordinary REGISTRATION\n * drift, which `registrationStaleAt()` reports and `repairRegistration()` fixes by removing it.\n *\n * The new fourth is `.codex/hooks.json`, and it is here because of what happened while it was NOT a\n * managed surface: something else wrote it, with a matcher that matched no Codex file tool and a shim\n * path anchored on a variable Codex does not set, and no check in this package could see it. A file the\n * guards depend on and nothing validates is the exact shape of that incident.\n *\n * Each harness's registration surface is its own name, from HarnessRegistration.registrationSurface, so\n * the deny sends the reader to the file that actually moved.\n */\nexport const SHIM_SURFACE = SHIM_MARKER;\nexport const REGISTRATION_SURFACE = CLAUDE_REGISTRATION.registrationSurface;\nexport const CODEX_REGISTRATION_SURFACE = CODEX_REGISTRATION.registrationSurface;\nexport const ENV_SURFACE = `.claude/settings.json env.${BASH_CWD_ENV_KEY}`;\n\n/**\n * WHICH of the managed surfaces disagree with this release — the input to fault S.\n *\n * All of them are checked against the SAME root, resolved from the RUNNING MODULE (governingShimRoot),\n * never from cwd and never from `$CLAUDE_PROJECT_DIR`: the files we compare and the renderers we\n * compare them TO must come from one install, or the check straddles two trees and can never converge.\n */\n// webpieces-disable no-function-outside-class -- module-scope for the same dependency-free reason as HarnessRegistration's siblings\nexport function managedSurfaceDrift(root: string | null): readonly string[] {\n const drifted: string[] = [];\n if (committedShimStale(root)) drifted.push(SHIM_SURFACE);\n for (const harness of HARNESS_REGISTRATIONS) {\n if (registrationStaleAt(harness, root)) drifted.push(harness.registrationSurface);\n // A neighbour hook registered with a RELATIVE entry path dies from any cwd but the root, and per\n // the hooks reference that non-zero exit is a NON-BLOCKING error — a SILENT UNGUARDED ALLOW. It\n // is a drift surface for exactly the reason the managed registration is one: nothing else in\n // this repo can see it, and the way it fails looks like a guard that PASSED. See\n // neighbour-hooks.ts, and issue #852 where it silently disarmed three security guards.\n if (neighbourHooksStaleAt(harness, root)) drifted.push(harness.neighbourSurface);\n }\n if (envStaleAt(root)) drifted.push(ENV_SURFACE);\n return drifted;\n}\n"]}
@@ -0,0 +1,42 @@
1
+ import type { ClaudeSettings } from './settings-shape';
2
+ /**
3
+ * How a drifted NEIGHBOUR surface is spelled, and the ONE place that spelling lives.
4
+ *
5
+ * `HarnessRegistration.neighbourSurface` builds a name ending in this, and the fault-S deny tests the
6
+ * drift list against it to decide whether to spend a line teaching this failure mode. Two literals would
7
+ * be two spellings of one thing: the deny would go quietly wrong — printing nothing for the one surface
8
+ * it exists to explain — with no test able to see it.
9
+ */
10
+ export declare const NEIGHBOUR_SURFACE_SUFFIX = "relative hook commands";
11
+ /**
12
+ * Rewrites the repo-relative entry paths in ONE hook command to `<anchor>/<path>`.
13
+ *
14
+ * `anchor` is the harness's own — `$CLAUDE_PROJECT_DIR` for Claude Code, `$PWD` for Codex — so a
15
+ * neighbour hook is anchored exactly the way webpieces anchors its own, and the MAIN tree governs every
16
+ * tree for the consumer's guards too.
17
+ */
18
+ export declare class NeighbourHookAnchor {
19
+ private readonly anchor;
20
+ private readonly root;
21
+ constructor(anchor: string, root: string);
22
+ /** The command with every anchorable token anchored; returns the input unchanged when there is none. */
23
+ rewrite(command: string): string;
24
+ /** The anchored spelling of one token, or null when this token must be left exactly as it is. */
25
+ private anchorToken;
26
+ }
27
+ /**
28
+ * Anchor every neighbour hook command IN PLACE; returns the NEW spelling of each one that changed.
29
+ *
30
+ * The return value is the rewritten commands rather than a boolean so the cure can print what it did to
31
+ * a file it does not own. A cure that silently edits a consumer's own hook lines is worse than one that
32
+ * does not edit them at all.
33
+ */
34
+ export declare function anchorNeighbourHooks(anchor: string, settings: ClaudeSettings, root: string): readonly string[];
35
+ /**
36
+ * True when this settings file carries a neighbour hook the repair WOULD anchor.
37
+ *
38
+ * Gated on `registeredBins()` by its caller for the same reason `registrationStale()` is: a settings
39
+ * file that registers no webpieces hooks is not a webpieces install, and rewriting somebody's unrelated
40
+ * hooks in it would be webpieces editing a file it was never given.
41
+ */
42
+ export declare function neighbourHooksStale(anchor: string, settings: ClaudeSettings, root: string): boolean;
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NeighbourHookAnchor = exports.NEIGHBOUR_SURFACE_SUFFIX = void 0;
4
+ exports.anchorNeighbourHooks = anchorNeighbourHooks;
5
+ exports.neighbourHooksStale = neighbourHooksStale;
6
+ const tslib_1 = require("tslib");
7
+ const fs = tslib_1.__importStar(require("fs"));
8
+ const path = tslib_1.__importStar(require("path"));
9
+ const shim_1 = require("./shim");
10
+ /**
11
+ * NEIGHBOUR HOOKS — the hooks a CONSUMER repo registers in the SAME settings file webpieces manages,
12
+ * and the one thing about them webpieces must own: their entry path has to be ABSOLUTE.
13
+ *
14
+ * ─── The defect (issue #852, MEASURED in a consumer repo) ─────────────────────────────────────────
15
+ * A repo registered five of its own guards RELATIVE, beside webpieces' one absolute entry:
16
+ *
17
+ * { "type": "command", "command": "node \".claude/hooks/guard-deploy.mjs\"" }
18
+ *
19
+ * A relative entry path resolves against the HOOK PROCESS's cwd. The moment that cwd is not the repo
20
+ * root, node cannot resolve the entry module and the guard dies before running a line:
21
+ *
22
+ * $ cd <repo>/services && echo '{}' | node ".claude/hooks/guard-deploy.mjs"
23
+ * Error: Cannot find module '<repo>/services/.claude/hooks/guard-deploy.mjs'
24
+ *
25
+ * Per the hooks reference that non-zero exit is a NON-BLOCKING error — a SILENT UNGUARDED ALLOW. So
26
+ * every Bash call printed five `PreToolUse:Bash hook error` stack-trace fragments, carrying no rule
27
+ * name and no verdict, and then RAN ANYWAY. The guards that stopped running were the security ones: a
28
+ * cleartext-credentials blocker, a `gcloud` blocker (the CLI ignores GOOGLE_APPLICATION_CREDENTIALS and
29
+ * would otherwise run as the developer's OWNER account) and a raw-deploy blocker. That is the exact
30
+ * inverse of the fail-closed property those guards are documented to have.
31
+ *
32
+ * ─── Why webpieces owns the fix rather than the consumer ──────────────────────────────────────────
33
+ * 1. The consumer is told `.claude/settings.json` is GENERATED and must never be hand-edited, so it
34
+ * cannot fix its own registration without fighting `wp-upgrade-shim` on the next bump.
35
+ * 2. webpieces already learned this lesson for its OWN hook — the relative→absolute reversal documented
36
+ * at length in hook-registration.ts's header, which cost an entire guard layer (L-1) before it was
37
+ * reversed. That reasoning was never extended to the entries co-registered beside it, and the
38
+ * failure mode is identical because it is a property of the FILE, not of who wrote the line.
39
+ *
40
+ * ─── The conservative rule, and why the cure always converges ─────────────────────────────────────
41
+ * A token is anchored ONLY when every one of these holds: it is not already absolute, variable-anchored
42
+ * or `~`-anchored; it contains a `/`; it does not escape the root with `..`; and — the load-bearing
43
+ * one — `<root>/<token>` EXISTS ON DISK. That last test is what keeps `npm run build/foo` and every
44
+ * other slash-carrying non-path untouched, and it is why drift is defined as "the repair would change
45
+ * something" rather than "a relative-looking token is present": a token this module declines to anchor
46
+ * never becomes drift, so fault S can never name a surface its own cure cannot repair.
47
+ *
48
+ * This is deliberately NOT a second spelling of the managed registration. Anything under the managed
49
+ * directory — `MANAGED_DIR`, derived below from `SHIM_MARKER` itself rather than re-spelled — is skipped
50
+ * here, so every webpieces-owned command is left to `repairRegistration()`, which is the ONE place their
51
+ * spelling is defined. The test is the DIRECTORY and not `isManagedCommand()`: this module holds no edge
52
+ * back to hook-registration.ts, precisely so the file-import cycle stays broken.
53
+ */
54
+ /**
55
+ * The webpieces-MANAGED directory, derived from the shim's own path so there is never a second spelling
56
+ * of it. Everything under it is rewritten wholesale by `repairRegistration()`, which is the ONE place
57
+ * the managed commands' shape is defined — anchoring a token there from here would be a second spelling
58
+ * of one registration, exactly the shim shape the compatibility policy rejects.
59
+ */
60
+ const MANAGED_DIR = `${shim_1.SHIM_MARKER.slice(0, shim_1.SHIM_MARKER.lastIndexOf('/'))}/`;
61
+ /**
62
+ * How a drifted NEIGHBOUR surface is spelled, and the ONE place that spelling lives.
63
+ *
64
+ * `HarnessRegistration.neighbourSurface` builds a name ending in this, and the fault-S deny tests the
65
+ * drift list against it to decide whether to spend a line teaching this failure mode. Two literals would
66
+ * be two spellings of one thing: the deny would go quietly wrong — printing nothing for the one surface
67
+ * it exists to explain — with no test able to see it.
68
+ */
69
+ exports.NEIGHBOUR_SURFACE_SUFFIX = 'relative hook commands';
70
+ /** A token that is already anchored (absolute, `$VAR`, `~`) or is a flag — never a relative entry path. */
71
+ const ALREADY_ANCHORED = /^[/~$-]/;
72
+ /** Quoted-or-bare token scan. Quotes are tried first so a quoted path is one token, not several. */
73
+ const COMMAND_TOKEN = /"([^"]*)"|'([^']*)'|(\S+)/g;
74
+ /**
75
+ * Rewrites the repo-relative entry paths in ONE hook command to `<anchor>/<path>`.
76
+ *
77
+ * `anchor` is the harness's own — `$CLAUDE_PROJECT_DIR` for Claude Code, `$PWD` for Codex — so a
78
+ * neighbour hook is anchored exactly the way webpieces anchors its own, and the MAIN tree governs every
79
+ * tree for the consumer's guards too.
80
+ */
81
+ class NeighbourHookAnchor {
82
+ anchor;
83
+ root;
84
+ constructor(anchor, root) {
85
+ this.anchor = anchor;
86
+ this.root = root;
87
+ }
88
+ /** The command with every anchorable token anchored; returns the input unchanged when there is none. */
89
+ rewrite(command) {
90
+ return command.replace(COMMAND_TOKEN, (whole, dq, sq, bare) => {
91
+ const token = dq ?? sq ?? bare ?? '';
92
+ const anchored = this.anchorToken(token);
93
+ if (anchored === null)
94
+ return whole;
95
+ if (sq !== undefined)
96
+ return `'${anchored}'`;
97
+ // A bare token is QUOTED on the way out, not left bare: the anchor expands to a real
98
+ // filesystem path, and a checkout under a directory with a space in it would otherwise be
99
+ // split into two arguments — which fails exactly as silently as the bug being fixed.
100
+ return `"${anchored}"`;
101
+ });
102
+ }
103
+ /** The anchored spelling of one token, or null when this token must be left exactly as it is. */
104
+ anchorToken(token) {
105
+ if (token === '' || ALREADY_ANCHORED.test(token))
106
+ return null;
107
+ if (!token.includes('/') || token.includes('$') || token.includes('~'))
108
+ return null;
109
+ const rel = token.startsWith('./') ? token.slice(2) : token;
110
+ // `..` escapes the root, so the anchor would not name what the author meant. Leave it and let
111
+ // the drift check stay silent about it — a surface the cure cannot repair must never be reported.
112
+ if (rel === '' || rel.startsWith('../') || rel.startsWith(MANAGED_DIR))
113
+ return null;
114
+ if (!fs.existsSync(path.join(this.root, ...rel.split('/'))))
115
+ return null;
116
+ return `${this.anchor}/${rel}`;
117
+ }
118
+ }
119
+ exports.NeighbourHookAnchor = NeighbourHookAnchor;
120
+ /**
121
+ * Every hook entry in a settings file, across EVERY event (PreToolUse, PostToolUse, Stop, …).
122
+ *
123
+ * Not scoped to PreToolUse, because the defect is not: a `PostToolUse` hook registered relative dies the
124
+ * same way from the same cwd, and scoping the repair to one event would leave a half-fixed file that
125
+ * reports success — the failure shape upgrade-shim.ts's header exists to prevent.
126
+ */
127
+ // webpieces-disable no-function-outside-class -- module-scope sibling of hook-registration.ts's readers, in the deliberately dependency-free bin layer
128
+ function allHookCommands(settings) {
129
+ const events = settings.hooks;
130
+ if (events === undefined)
131
+ return [];
132
+ const found = [];
133
+ for (const entries of Object.values(events)) {
134
+ if (!Array.isArray(entries))
135
+ continue;
136
+ for (const entry of entries) {
137
+ if (Array.isArray(entry.hooks))
138
+ found.push(...entry.hooks);
139
+ }
140
+ }
141
+ return found;
142
+ }
143
+ /**
144
+ * Anchor every neighbour hook command IN PLACE; returns the NEW spelling of each one that changed.
145
+ *
146
+ * The return value is the rewritten commands rather than a boolean so the cure can print what it did to
147
+ * a file it does not own. A cure that silently edits a consumer's own hook lines is worse than one that
148
+ * does not edit them at all.
149
+ */
150
+ // webpieces-disable no-function-outside-class -- module-scope sibling of hook-registration.ts's repairs, in the deliberately dependency-free bin layer
151
+ function anchorNeighbourHooks(anchor, settings, root) {
152
+ const anchorer = new NeighbourHookAnchor(anchor, root);
153
+ const rewritten = [];
154
+ for (const hook of allHookCommands(settings)) {
155
+ if (typeof hook.command !== 'string')
156
+ continue;
157
+ const anchored = anchorer.rewrite(hook.command);
158
+ if (anchored === hook.command)
159
+ continue;
160
+ hook.command = anchored;
161
+ rewritten.push(anchored);
162
+ }
163
+ return rewritten;
164
+ }
165
+ /**
166
+ * True when this settings file carries a neighbour hook the repair WOULD anchor.
167
+ *
168
+ * Gated on `registeredBins()` by its caller for the same reason `registrationStale()` is: a settings
169
+ * file that registers no webpieces hooks is not a webpieces install, and rewriting somebody's unrelated
170
+ * hooks in it would be webpieces editing a file it was never given.
171
+ */
172
+ // webpieces-disable no-function-outside-class -- module-scope sibling of hook-registration.ts's drift checks, in the deliberately dependency-free bin layer
173
+ function neighbourHooksStale(anchor, settings, root) {
174
+ const anchorer = new NeighbourHookAnchor(anchor, root);
175
+ return allHookCommands(settings).some((h) => typeof h.command === 'string' && anchorer.rewrite(h.command) !== h.command);
176
+ }
177
+ //# sourceMappingURL=neighbour-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"neighbour-hooks.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/neighbour-hooks.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { SHIM_MARKER } from './shim';\n// The SHAPE only, from the leaf module both this file and hook-registration.ts point at. This module\n// deliberately knows nothing about `HarnessRegistration`: it takes the harness's ANCHOR as a string, so\n// there is no edge back to hook-registration at all — not even a type-only one, which\n// `validate-no-file-import-cycles` counts as a cycle just the same.\nimport type { ClaudeSettings, HookCommand, HookEntry } from './settings-shape';\n\n/**\n * NEIGHBOUR HOOKS — the hooks a CONSUMER repo registers in the SAME settings file webpieces manages,\n * and the one thing about them webpieces must own: their entry path has to be ABSOLUTE.\n *\n * ─── The defect (issue #852, MEASURED in a consumer repo) ─────────────────────────────────────────\n * A repo registered five of its own guards RELATIVE, beside webpieces' one absolute entry:\n *\n * { \"type\": \"command\", \"command\": \"node \\\".claude/hooks/guard-deploy.mjs\\\"\" }\n *\n * A relative entry path resolves against the HOOK PROCESS's cwd. The moment that cwd is not the repo\n * root, node cannot resolve the entry module and the guard dies before running a line:\n *\n * $ cd <repo>/services && echo '{}' | node \".claude/hooks/guard-deploy.mjs\"\n * Error: Cannot find module '<repo>/services/.claude/hooks/guard-deploy.mjs'\n *\n * Per the hooks reference that non-zero exit is a NON-BLOCKING error — a SILENT UNGUARDED ALLOW. So\n * every Bash call printed five `PreToolUse:Bash hook error` stack-trace fragments, carrying no rule\n * name and no verdict, and then RAN ANYWAY. The guards that stopped running were the security ones: a\n * cleartext-credentials blocker, a `gcloud` blocker (the CLI ignores GOOGLE_APPLICATION_CREDENTIALS and\n * would otherwise run as the developer's OWNER account) and a raw-deploy blocker. That is the exact\n * inverse of the fail-closed property those guards are documented to have.\n *\n * ─── Why webpieces owns the fix rather than the consumer ──────────────────────────────────────────\n * 1. The consumer is told `.claude/settings.json` is GENERATED and must never be hand-edited, so it\n * cannot fix its own registration without fighting `wp-upgrade-shim` on the next bump.\n * 2. webpieces already learned this lesson for its OWN hook — the relative→absolute reversal documented\n * at length in hook-registration.ts's header, which cost an entire guard layer (L-1) before it was\n * reversed. That reasoning was never extended to the entries co-registered beside it, and the\n * failure mode is identical because it is a property of the FILE, not of who wrote the line.\n *\n * ─── The conservative rule, and why the cure always converges ─────────────────────────────────────\n * A token is anchored ONLY when every one of these holds: it is not already absolute, variable-anchored\n * or `~`-anchored; it contains a `/`; it does not escape the root with `..`; and — the load-bearing\n * one — `<root>/<token>` EXISTS ON DISK. That last test is what keeps `npm run build/foo` and every\n * other slash-carrying non-path untouched, and it is why drift is defined as \"the repair would change\n * something\" rather than \"a relative-looking token is present\": a token this module declines to anchor\n * never becomes drift, so fault S can never name a surface its own cure cannot repair.\n *\n * This is deliberately NOT a second spelling of the managed registration. Anything under the managed\n * directory — `MANAGED_DIR`, derived below from `SHIM_MARKER` itself rather than re-spelled — is skipped\n * here, so every webpieces-owned command is left to `repairRegistration()`, which is the ONE place their\n * spelling is defined. The test is the DIRECTORY and not `isManagedCommand()`: this module holds no edge\n * back to hook-registration.ts, precisely so the file-import cycle stays broken.\n */\n\n/**\n * The webpieces-MANAGED directory, derived from the shim's own path so there is never a second spelling\n * of it. Everything under it is rewritten wholesale by `repairRegistration()`, which is the ONE place\n * the managed commands' shape is defined — anchoring a token there from here would be a second spelling\n * of one registration, exactly the shim shape the compatibility policy rejects.\n */\nconst MANAGED_DIR = `${SHIM_MARKER.slice(0, SHIM_MARKER.lastIndexOf('/'))}/`;\n\n/**\n * How a drifted NEIGHBOUR surface is spelled, and the ONE place that spelling lives.\n *\n * `HarnessRegistration.neighbourSurface` builds a name ending in this, and the fault-S deny tests the\n * drift list against it to decide whether to spend a line teaching this failure mode. Two literals would\n * be two spellings of one thing: the deny would go quietly wrong — printing nothing for the one surface\n * it exists to explain — with no test able to see it.\n */\nexport const NEIGHBOUR_SURFACE_SUFFIX = 'relative hook commands';\n\n/** A token that is already anchored (absolute, `$VAR`, `~`) or is a flag — never a relative entry path. */\nconst ALREADY_ANCHORED = /^[/~$-]/;\n\n/** Quoted-or-bare token scan. Quotes are tried first so a quoted path is one token, not several. */\nconst COMMAND_TOKEN = /\"([^\"]*)\"|'([^']*)'|(\\S+)/g;\n\n/**\n * Rewrites the repo-relative entry paths in ONE hook command to `<anchor>/<path>`.\n *\n * `anchor` is the harness's own — `$CLAUDE_PROJECT_DIR` for Claude Code, `$PWD` for Codex — so a\n * neighbour hook is anchored exactly the way webpieces anchors its own, and the MAIN tree governs every\n * tree for the consumer's guards too.\n */\nexport class NeighbourHookAnchor {\n constructor(\n private readonly anchor: string,\n private readonly root: string,\n ) {}\n\n /** The command with every anchorable token anchored; returns the input unchanged when there is none. */\n rewrite(command: string): string {\n return command.replace(COMMAND_TOKEN, (whole: string, dq?: string, sq?: string, bare?: string): string => {\n const token = dq ?? sq ?? bare ?? '';\n const anchored = this.anchorToken(token);\n if (anchored === null) return whole;\n if (sq !== undefined) return `'${anchored}'`;\n // A bare token is QUOTED on the way out, not left bare: the anchor expands to a real\n // filesystem path, and a checkout under a directory with a space in it would otherwise be\n // split into two arguments — which fails exactly as silently as the bug being fixed.\n return `\"${anchored}\"`;\n });\n }\n\n /** The anchored spelling of one token, or null when this token must be left exactly as it is. */\n private anchorToken(token: string): string | null {\n if (token === '' || ALREADY_ANCHORED.test(token)) return null;\n if (!token.includes('/') || token.includes('$') || token.includes('~')) return null;\n const rel = token.startsWith('./') ? token.slice(2) : token;\n // `..` escapes the root, so the anchor would not name what the author meant. Leave it and let\n // the drift check stay silent about it — a surface the cure cannot repair must never be reported.\n if (rel === '' || rel.startsWith('../') || rel.startsWith(MANAGED_DIR)) return null;\n if (!fs.existsSync(path.join(this.root, ...rel.split('/')))) return null;\n return `${this.anchor}/${rel}`;\n }\n}\n\n/**\n * Every hook entry in a settings file, across EVERY event (PreToolUse, PostToolUse, Stop, …).\n *\n * Not scoped to PreToolUse, because the defect is not: a `PostToolUse` hook registered relative dies the\n * same way from the same cwd, and scoping the repair to one event would leave a half-fixed file that\n * reports success — the failure shape upgrade-shim.ts's header exists to prevent.\n */\n// webpieces-disable no-function-outside-class -- module-scope sibling of hook-registration.ts's readers, in the deliberately dependency-free bin layer\nfunction allHookCommands(settings: ClaudeSettings): readonly HookCommand[] {\n const events = settings.hooks;\n if (events === undefined) return [];\n const found: HookCommand[] = [];\n for (const entries of Object.values(events)) {\n if (!Array.isArray(entries)) continue;\n for (const entry of entries as readonly HookEntry[]) {\n if (Array.isArray(entry.hooks)) found.push(...entry.hooks);\n }\n }\n return found;\n}\n\n/**\n * Anchor every neighbour hook command IN PLACE; returns the NEW spelling of each one that changed.\n *\n * The return value is the rewritten commands rather than a boolean so the cure can print what it did to\n * a file it does not own. A cure that silently edits a consumer's own hook lines is worse than one that\n * does not edit them at all.\n */\n// webpieces-disable no-function-outside-class -- module-scope sibling of hook-registration.ts's repairs, in the deliberately dependency-free bin layer\nexport function anchorNeighbourHooks(anchor: string, settings: ClaudeSettings, root: string): readonly string[] {\n const anchorer = new NeighbourHookAnchor(anchor, root);\n const rewritten: string[] = [];\n for (const hook of allHookCommands(settings)) {\n if (typeof hook.command !== 'string') continue;\n const anchored = anchorer.rewrite(hook.command);\n if (anchored === hook.command) continue;\n hook.command = anchored;\n rewritten.push(anchored);\n }\n return rewritten;\n}\n\n/**\n * True when this settings file carries a neighbour hook the repair WOULD anchor.\n *\n * Gated on `registeredBins()` by its caller for the same reason `registrationStale()` is: a settings\n * file that registers no webpieces hooks is not a webpieces install, and rewriting somebody's unrelated\n * hooks in it would be webpieces editing a file it was never given.\n */\n// webpieces-disable no-function-outside-class -- module-scope sibling of hook-registration.ts's drift checks, in the deliberately dependency-free bin layer\nexport function neighbourHooksStale(anchor: string, settings: ClaudeSettings, root: string): boolean {\n const anchorer = new NeighbourHookAnchor(anchor, root);\n return allHookCommands(settings).some((h: HookCommand): boolean => typeof h.command === 'string' && anchorer.rewrite(h.command) !== h.command);\n}\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * THE SETTINGS-FILE SHAPE — one declaration of it, in a LEAF module that imports nothing.
3
+ *
4
+ * MEASURED: Codex's `.codex/hooks.json` uses the IDENTICAL `hooks.<event>[].hooks[].command` shape as
5
+ * Claude Code's `.claude/settings.json`, so one reader, one writer and one repair serve both files —
6
+ * the difference between the harnesses is entirely in the VALUES (matcher, shim anchor), which is what
7
+ * `HarnessRegistration` carries. The `env` block is Claude Code's alone.
8
+ *
9
+ * It sits in its own file because two modules need it and one of them, `hook-registration.ts`, imports
10
+ * the repairs from the other, `neighbour-hooks.ts`. Declaring the shape in either one would close a file
11
+ * import cycle — which `validate-no-file-import-cycles` fails the build on, and which is a genuine
12
+ * hazard in this bin layer, where every module must still load on a tree too broken to build anything.
13
+ * A leaf both can point at costs one file and removes the choice.
14
+ */
15
+ export interface HookCommand {
16
+ type: string;
17
+ command: string;
18
+ }
19
+ export interface HookEntry {
20
+ matcher: string;
21
+ hooks: HookCommand[];
22
+ }
23
+ /**
24
+ * The `hooks` block, keyed by EVENT. `PreToolUse` is named because every webpieces-managed entry lives
25
+ * there; the index signature is what lets the neighbour-anchoring pass reach `PostToolUse`, `Stop` and
26
+ * everything else — a relative entry path fails to load under any event, so a repair scoped to one of
27
+ * them would leave a half-fixed file and report success.
28
+ */
29
+ export interface HookEvents {
30
+ PreToolUse?: HookEntry[];
31
+ [event: string]: HookEntry[] | undefined;
32
+ }
33
+ export interface ClaudeSettings {
34
+ hooks?: HookEvents;
35
+ env?: Record<string, string>;
36
+ [key: string]: unknown;
37
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ /**
3
+ * THE SETTINGS-FILE SHAPE — one declaration of it, in a LEAF module that imports nothing.
4
+ *
5
+ * MEASURED: Codex's `.codex/hooks.json` uses the IDENTICAL `hooks.<event>[].hooks[].command` shape as
6
+ * Claude Code's `.claude/settings.json`, so one reader, one writer and one repair serve both files —
7
+ * the difference between the harnesses is entirely in the VALUES (matcher, shim anchor), which is what
8
+ * `HarnessRegistration` carries. The `env` block is Claude Code's alone.
9
+ *
10
+ * It sits in its own file because two modules need it and one of them, `hook-registration.ts`, imports
11
+ * the repairs from the other, `neighbour-hooks.ts`. Declaring the shape in either one would close a file
12
+ * import cycle — which `validate-no-file-import-cycles` fails the build on, and which is a genuine
13
+ * hazard in this bin layer, where every module must still load on a tree too broken to build anything.
14
+ * A leaf both can point at costs one file and removes the choice.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ //# sourceMappingURL=settings-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings-shape.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/settings-shape.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG","sourcesContent":["/**\n * THE SETTINGS-FILE SHAPE — one declaration of it, in a LEAF module that imports nothing.\n *\n * MEASURED: Codex's `.codex/hooks.json` uses the IDENTICAL `hooks.<event>[].hooks[].command` shape as\n * Claude Code's `.claude/settings.json`, so one reader, one writer and one repair serve both files —\n * the difference between the harnesses is entirely in the VALUES (matcher, shim anchor), which is what\n * `HarnessRegistration` carries. The `env` block is Claude Code's alone.\n *\n * It sits in its own file because two modules need it and one of them, `hook-registration.ts`, imports\n * the repairs from the other, `neighbour-hooks.ts`. Declaring the shape in either one would close a file\n * import cycle — which `validate-no-file-import-cycles` fails the build on, and which is a genuine\n * hazard in this bin layer, where every module must still load on a tree too broken to build anything.\n * A leaf both can point at costs one file and removes the choice.\n */\n\n// webpieces-disable no-any-unknown -- settings.json is opaque consumer JSON\nexport interface HookCommand { type: string; command: string; }\n\nexport interface HookEntry { matcher: string; hooks: HookCommand[]; }\n\n/**\n * The `hooks` block, keyed by EVENT. `PreToolUse` is named because every webpieces-managed entry lives\n * there; the index signature is what lets the neighbour-anchoring pass reach `PostToolUse`, `Stop` and\n * everything else — a relative entry path fails to load under any event, so a repair scoped to one of\n * them would leave a half-fixed file and report success.\n */\nexport interface HookEvents {\n PreToolUse?: HookEntry[];\n [event: string]: HookEntry[] | undefined;\n}\n\nexport interface ClaudeSettings {\n hooks?: HookEvents;\n // Claude Code's settings `env` block: every key is exported into the environment of the session AND\n // of every subagent it spawns. That inheritance is precisely why webpieces pins its managed entry\n // here rather than in a shell profile — see managed-env.ts.\n env?: Record<string, string>;\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n"]}
@@ -1,4 +1,5 @@
1
- import { ClaudeSettings, HarnessRegistration } from './hook-registration';
1
+ import { HarnessRegistration } from './hook-registration';
2
+ import type { ClaudeSettings } from './settings-shape';
2
3
  declare class HookSpec {
3
4
  readonly key: string;
4
5
  readonly label: string;