@webpieces/ai-hook-rules 0.4.710 → 0.4.712

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.
Files changed (59) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/detect-ai.d.ts +5 -4
  3. package/src/adapters/detect-ai.js +5 -4
  4. package/src/adapters/detect-ai.js.map +1 -1
  5. package/src/adapters/hook-core.js +9 -3
  6. package/src/adapters/hook-core.js.map +1 -1
  7. package/src/bin/codex-guard-presence.d.ts +86 -0
  8. package/src/bin/codex-guard-presence.js +150 -0
  9. package/src/bin/codex-guard-presence.js.map +1 -0
  10. package/src/bin/codex-trust.d.ts +99 -0
  11. package/src/bin/codex-trust.js +199 -0
  12. package/src/bin/codex-trust.js.map +1 -0
  13. package/src/bin/hook-registration.d.ts +200 -43
  14. package/src/bin/hook-registration.js +218 -89
  15. package/src/bin/hook-registration.js.map +1 -1
  16. package/src/bin/l0-allowlist.d.ts +0 -30
  17. package/src/bin/l0-allowlist.js +9 -67
  18. package/src/bin/l0-allowlist.js.map +1 -1
  19. package/src/bin/l0-decide.d.ts +43 -0
  20. package/src/bin/l0-decide.js +90 -0
  21. package/src/bin/l0-decide.js.map +1 -0
  22. package/src/bin/l0-ignored-tools.d.ts +38 -0
  23. package/src/bin/l0-ignored-tools.js +44 -0
  24. package/src/bin/l0-ignored-tools.js.map +1 -0
  25. package/src/bin/setup-config.d.ts +19 -0
  26. package/src/bin/setup-config.js +370 -0
  27. package/src/bin/setup-config.js.map +1 -0
  28. package/src/bin/setup.d.ts +56 -21
  29. package/src/bin/setup.js +125 -370
  30. package/src/bin/setup.js.map +1 -1
  31. package/src/bin/shim-audit-log.js +10 -1
  32. package/src/bin/shim-audit-log.js.map +1 -1
  33. package/src/bin/shim-deny-reason.js +4 -4
  34. package/src/bin/shim-deny-reason.js.map +1 -1
  35. package/src/bin/shim.d.ts +2 -0
  36. package/src/bin/shim.js +21 -1
  37. package/src/bin/shim.js.map +1 -1
  38. package/src/bin/upgrade-shim.d.ts +1 -1
  39. package/src/bin/upgrade-shim.js +12 -8
  40. package/src/bin/upgrade-shim.js.map +1 -1
  41. package/src/core/agent-event.d.ts +16 -0
  42. package/src/core/agent-event.js +17 -1
  43. package/src/core/agent-event.js.map +1 -1
  44. package/src/core/ai-type-context.d.ts +36 -0
  45. package/src/core/ai-type-context.js +45 -0
  46. package/src/core/ai-type-context.js.map +1 -0
  47. package/src/core/decision-log.js +9 -0
  48. package/src/core/decision-log.js.map +1 -1
  49. package/src/core/l0-matrix.js +14 -6
  50. package/src/core/l0-matrix.js.map +1 -1
  51. package/src/core/l0-tooling-doc.d.ts +3 -2
  52. package/src/core/l0-tooling-doc.js +22 -10
  53. package/src/core/l0-tooling-doc.js.map +1 -1
  54. package/src/core/rejection-log.js +5 -1
  55. package/src/core/rejection-log.js.map +1 -1
  56. package/src/index.d.ts +1 -1
  57. package/src/index.js +4 -1
  58. package/src/index.js.map +1 -1
  59. package/templates/ai-hook.sh +15 -1
@@ -1,10 +1,19 @@
1
+ import { AiType } from '../core/agent-event';
1
2
  /**
2
- * THE INSTALLED HOOK SURFACE — two hooks, both ABSOLUTE, and the ONE place their spelling is defined.
3
+ * THE INSTALLED HOOK SURFACE — two hooks per harness, all ABSOLUTE, and the ONE place their spelling is
4
+ * defined. See `HarnessRegistration` below, which is what makes "per harness" data rather than four
5
+ * module constants that were only ever true of Claude Code.
3
6
  *
4
7
  * ─── ONE GOVERNOR: the MAIN tree judges every tree ─────────────────────────────────────────────────
5
8
  *
6
- * H1 sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" wp-ai-guards-hook Write|…|Bash|Read
7
- * H2 sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" wp-ai-rules-hook Write|Edit|MultiEdit
9
+ * Claude Code — .claude/settings.json
10
+ * H1 sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" wp-ai-guards-hook Write|…|Bash|Read
11
+ * H2 sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" wp-ai-rules-hook Write|Edit|MultiEdit
12
+ * Codex — .codex/hooks.json (the SAME `hooks.PreToolUse` JSON shape, measured)
13
+ * H1 sh "$PWD/.claude/webpieces/ai-hook.sh" wp-ai-guards-hook Bash|apply_patch
14
+ * H2 sh "$PWD/.claude/webpieces/ai-hook.sh" wp-ai-rules-hook apply_patch
15
+ *
16
+ * ONE shim file serves all four: `.claude/` there is a path, not a claim about who is calling.
8
17
  *
9
18
  * This REPLACES a three-hook form in which these two were RELATIVE (`sh ".claude/webpieces/ai-hook.sh"`)
10
19
  * and a third hook, L-1 `guarantee-root.sh`, existed solely to guarantee that relative path resolved.
@@ -81,6 +90,12 @@ export interface HookEntry {
81
90
  matcher: string;
82
91
  hooks: HookCommand[];
83
92
  }
93
+ /**
94
+ * One settings file as webpieces reads it. MEASURED: Codex's `.codex/hooks.json` uses the IDENTICAL
95
+ * `hooks.PreToolUse[].hooks[].command` shape, so one reader, one writer and one repair serve both files
96
+ * — the difference between the harnesses is entirely in the VALUES (matcher, shim anchor), which is what
97
+ * `HarnessRegistration` carries. The `env` block below is Claude Code's alone.
98
+ */
84
99
  export interface ClaudeSettings {
85
100
  hooks?: {
86
101
  PreToolUse?: HookEntry[];
@@ -90,34 +105,147 @@ export interface ClaudeSettings {
90
105
  }
91
106
  export declare const RULES_BIN = "wp-ai-rules-hook";
92
107
  export declare const GUARDS_BIN = "wp-ai-guards-hook";
93
- export declare const RULES_MATCHER = "Write|Edit|MultiEdit";
94
- export declare const GUARDS_MATCHER = "Write|Edit|MultiEdit|Bash|Read";
95
108
  /**
96
- * The guard-hook command`sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" <bin>`.
109
+ * ONE HARNESS'S registration surface where its hooks live, what they match, and how they name the
110
+ * shim. Data-only → a class, per CLAUDE.md.
97
111
  *
98
- * ABSOLUTE. This replaced a RELATIVE spelling, and the reversal is the whole point of this release.
112
+ * ─── Why this exists ──────────────────────────────────────────────────────────────────────────────
113
+ * The matchers and the shim command used to be four module constants written for Claude Code alone.
114
+ * Applied to Codex they are wrong in both halves, and the way they are wrong is SILENT:
99
115
  *
100
- * The relative form existed to give each git tree its own release, binary and pin. **It never delivered
101
- * that.** Measured 2026-08-10: a linked worktree has no `node_modules`, so ai-hook.sh's upward walk
102
- * executes the PRIMARY's binary `readlink -f` resolved a worktree agent's bin to
103
- * `<primary>/node_modules/@webpieces/ai-hook-rules`. A worktree ran its own SCRIPT and its own CONFIG,
104
- * never its own release. The property was fiction, and paying for it cost an entire guard layer: a
105
- * relative path only resolves at a tree root, so L-1 (guarantee-root.sh) had to deny every `cd` into a
106
- * project subdirectory, which produced the force-to-root bug class, the reaped-worktree `cd`
107
- * prescription, and a measured hard deadlock where L-1 told a worktree-isolated agent to `cd` to the
108
- * primary clone — which the harness refuses for an isolated agent.
116
+ * - MATCHER. Codex's file-editing tool is `apply_patch` (MEASURED, codex-cli 0.151.0). A matcher of
117
+ * `Write|Edit|MultiEdit` matches it never, so every file rule is unreachable while the settings file
118
+ * looks perfectly installed. Its shell tool IS called `Bash` Codex reuses Claude's name — which is
119
+ * the trap: half the matcher works, so the hooks appear to be running.
120
+ * - ANCHOR. `$CLAUDE_PROJECT_DIR` does not exist in a Codex hook's environment (measured: 46 vars, no
121
+ * such key), so the command expands to `sh "/.claude/webpieces/ai-hook.sh"`, which dies and per the
122
+ * hooks protocol a non-2 non-zero exit is a NON-BLOCKING error, i.e. a silent unguarded allow.
109
123
  *
110
- * Absolute resolves from ANY cwd, so L-1 has no job left and is deleted. One governor: the MAIN tree
111
- * judges every tree, which is what was already happening via the borrowed binary the design now says
112
- * so out loud, and `VersionSyncGuard` blocks the case where that is the wrong answer.
124
+ * Both halves of that were live in real repos, written by a Codex Desktop sync that transliterated the
125
+ * Claude setup. Making the registration per-harness DATA is what stops a future harness inheriting a
126
+ * matcher that was never true for it.
113
127
  *
114
- * Invoked via `sh <file>` rather than executed directly so a missing executable bit on the checked-in
115
- * shim (fresh clone, a filesystem that drops the bit, git core.fileMode quirks) can never break the hook
116
- * with a raw `Permission denied` on every tool call. Quoted to survive spaces in the path.
128
+ * ─── ONE SHIM, both harnesses ─────────────────────────────────────────────────────────────────────
129
+ * `SHIM_MARKER` is shared deliberately: `.claude/webpieces/ai-hook.sh` is the single fail-closed entry
130
+ * point, and moving or duplicating it would double the L0 allowlist regexes, the drift surfaces and the
131
+ * cures. The `.claude/` prefix is a path, not a claim about which agent is calling.
117
132
  */
118
- export declare function shimCommand(bin: string): string;
119
- /** The registration entry for one guard bin. */
120
- export declare function guardHookEntry(bin: string): HookRegistrationEntry;
133
+ export declare class HarnessRegistration {
134
+ readonly aiType: AiType;
135
+ /** How the installer and the drift report name this harness to a human. */
136
+ readonly label: string;
137
+ /** Which tool names the RULES hook (code-style, file-scoped) must see. */
138
+ readonly rulesMatcher: string;
139
+ /**
140
+ * Which tool names the GUARDS hook must see. Wider than the rules matcher by the shell tool and,
141
+ * for Claude Code, by `Read` — Read carries no guard, but the guards hook owns the
142
+ * per-invocation audit log, so matching it records every file the AI opens (log-and-allow fast
143
+ * path in hook-core.ts; a Read is never blocked). Codex has no Read tool at all: a read arrives
144
+ * as `Bash` running a pager, which the shell matcher already covers and read-parity synthesizes.
145
+ */
146
+ readonly guardsMatcher: string;
147
+ /** The settings files this harness's hooks can be installed into, relative to the repo root. */
148
+ readonly settingsFiles: readonly string[];
149
+ /**
150
+ * THE PREFIX THE SHIM PATH IS ANCHORED ON, and the reason each harness needs its own.
151
+ *
152
+ * **BYTE-STABILITY IS A HARD CONSTRAINT ON THE CODEX VALUE.** Codex trusts a hook entry
153
+ * TOFU — `~/.codex/config.toml` records a `trusted_hash` per entry — so ANY change to these bytes
154
+ * invalidates that trust and re-prompts the human, whose third option is `Continue without
155
+ * trusting (hooks won't run)`: one keystroke to a silently unguarded session. Changing this
156
+ * string is therefore never a cosmetic edit. The hash is NOT reproducible from outside Codex
157
+ * (16 encodings tried against a file we authored), so the installer can never repair trust for
158
+ * itself — see ./codex-trust.ts, which REPORTS and never writes.
159
+ */
160
+ readonly shimAnchor: string;
161
+ /**
162
+ * True when this harness's settings file also carries the managed `env` block. Claude Code's
163
+ * settings `env` is inherited by every subagent, which is what makes it the right home for the
164
+ * Bash-cwd pin (see managed-env.ts). Codex has no equivalent surface — and needs none: its cwd
165
+ * is MEASURED not to drift at all (`cd x && pwd` prints x, the next call is back at the repo
166
+ * root, for the coordinator and for subagents alike).
167
+ */
168
+ readonly managesEnv: boolean;
169
+ /** What `managedSurfaceDrift()` calls this harness's registration when it has moved. */
170
+ readonly registrationSurface: string;
171
+ constructor(aiType: AiType,
172
+ /** How the installer and the drift report name this harness to a human. */
173
+ label: string,
174
+ /** Which tool names the RULES hook (code-style, file-scoped) must see. */
175
+ rulesMatcher: string,
176
+ /**
177
+ * Which tool names the GUARDS hook must see. Wider than the rules matcher by the shell tool and,
178
+ * for Claude Code, by `Read` — Read carries no guard, but the guards hook owns the
179
+ * per-invocation audit log, so matching it records every file the AI opens (log-and-allow fast
180
+ * path in hook-core.ts; a Read is never blocked). Codex has no Read tool at all: a read arrives
181
+ * as `Bash` running a pager, which the shell matcher already covers and read-parity synthesizes.
182
+ */
183
+ guardsMatcher: string,
184
+ /** The settings files this harness's hooks can be installed into, relative to the repo root. */
185
+ settingsFiles: readonly string[],
186
+ /**
187
+ * THE PREFIX THE SHIM PATH IS ANCHORED ON, and the reason each harness needs its own.
188
+ *
189
+ * **BYTE-STABILITY IS A HARD CONSTRAINT ON THE CODEX VALUE.** Codex trusts a hook entry
190
+ * TOFU — `~/.codex/config.toml` records a `trusted_hash` per entry — so ANY change to these bytes
191
+ * invalidates that trust and re-prompts the human, whose third option is `Continue without
192
+ * trusting (hooks won't run)`: one keystroke to a silently unguarded session. Changing this
193
+ * string is therefore never a cosmetic edit. The hash is NOT reproducible from outside Codex
194
+ * (16 encodings tried against a file we authored), so the installer can never repair trust for
195
+ * itself — see ./codex-trust.ts, which REPORTS and never writes.
196
+ */
197
+ shimAnchor: string,
198
+ /**
199
+ * True when this harness's settings file also carries the managed `env` block. Claude Code's
200
+ * settings `env` is inherited by every subagent, which is what makes it the right home for the
201
+ * Bash-cwd pin (see managed-env.ts). Codex has no equivalent surface — and needs none: its cwd
202
+ * is MEASURED not to drift at all (`cd x && pwd` prints x, the next call is back at the repo
203
+ * root, for the coordinator and for subagents alike).
204
+ */
205
+ managesEnv: boolean,
206
+ /** What `managedSurfaceDrift()` calls this harness's registration when it has moved. */
207
+ registrationSurface: string);
208
+ /** Which matcher one guard bin registers under. */
209
+ matcherFor(bin: string): string;
210
+ /**
211
+ * The guard-hook command — `sh "<anchor>/.claude/webpieces/ai-hook.sh" <bin>`.
212
+ *
213
+ * ABSOLUTE, in both harnesses. Claude's anchor replaced a RELATIVE spelling and that reversal is
214
+ * documented at length in this file's header: relative resolves only at a tree root, a hook that
215
+ * cannot resolve exits 127, and per the hooks reference that is a NON-BLOCKING error — a silent
216
+ * unguarded allow. It also cost an entire guard layer (L-1) whose only job was to police the `cd`
217
+ * that made the relative path resolvable.
218
+ *
219
+ * Invoked via `sh <file>` rather than executed directly so a missing executable bit on the
220
+ * checked-in shim (fresh clone, a filesystem that drops the bit, git core.fileMode quirks) can never
221
+ * break the hook with a raw `Permission denied` on every tool call. Quoted to survive spaces.
222
+ */
223
+ shimCommand(bin: string): string;
224
+ /** The registration entry for one guard bin under this harness. */
225
+ entryFor(bin: string): HookRegistrationEntry;
226
+ /** This harness's settings files under one repo root, absolute. */
227
+ settingsPaths(projectRoot: string): readonly string[];
228
+ }
229
+ export declare const CLAUDE_REGISTRATION: HarnessRegistration;
230
+ /**
231
+ * Codex's registration, every value of it MEASURED against codex-cli 0.151.0 rather than assumed.
232
+ *
233
+ * `$PWD` is the anchor because there is no project-dir variable to use and none is needed: the payload
234
+ * `cwd` and the hook process's own `PWD` are both the repo root on EVERY call — for the coordinator and
235
+ * for subagents — and a `cd` inside one command never survives into the next. That was measured, not
236
+ * hoped for, and it is the same effect Claude Code gets from the managed
237
+ * `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` env entry. See `shimAnchor` for why these exact bytes are
238
+ * not free to change.
239
+ */
240
+ export declare const CODEX_REGISTRATION: HarnessRegistration;
241
+ /**
242
+ * Every harness webpieces arms, in installer order.
243
+ *
244
+ * The drift check, the repair and the installer all iterate THIS, so a harness cannot be armed by the
245
+ * installer and then left unvalidated — which is exactly the state `.codex/hooks.json` was in before it
246
+ * was a managed surface: written by something else, silently wrong, and invisible to every check.
247
+ */
248
+ export declare const HARNESS_REGISTRATIONS: readonly HarnessRegistration[];
121
249
  /**
122
250
  * True when this PreToolUse command is one webpieces owns — in ANY spelling it has ever shipped.
123
251
  *
@@ -145,7 +273,7 @@ export declare function registeredBins(settings: ClaudeSettings): readonly strin
145
273
  * whenever the GUARDS bin was present; it is retired, and a settings file still carrying it is STALE —
146
274
  * `repairRegistration()` removes it via isManagedCommand()'s legacy marker.
147
275
  */
148
- export declare function expectedEntries(bins: readonly string[]): readonly HookRegistrationEntry[];
276
+ export declare function expectedEntries(harness: HarnessRegistration, bins: readonly string[]): readonly HookRegistrationEntry[];
149
277
  /**
150
278
  * True when a settings file registers webpieces hooks in a shape this release does not expect: a
151
279
  * RELATIVE shim command, a leftover guarantee-root entry, a stray duplicate, or a wrong matcher.
@@ -153,7 +281,7 @@ export declare function expectedEntries(bins: readonly string[]): readonly HookR
153
281
  * Compared as a SET, not a sequence: Claude Code runs all matching hooks in parallel, so array order
154
282
  * carries no meaning and reordering must not read as drift.
155
283
  */
156
- export declare function registrationStale(settings: ClaudeSettings): boolean;
284
+ export declare function registrationStale(harness: HarnessRegistration, settings: ClaudeSettings): boolean;
157
285
  /** Drop every webpieces-managed PreToolUse command; returns true if anything was removed. */
158
286
  export declare function dropManagedEntries(settings: ClaudeSettings): boolean;
159
287
  /** Append one PreToolUse entry. */
@@ -199,14 +327,28 @@ export declare function applyManagedEnv(settings: ClaudeSettings): boolean;
199
327
  * have been reported as drifted by fault S and then left unrepaired by its own prescribed cure. A cure
200
328
  * that skips half the surface is the failure mode `upgrade-shim.ts`'s header exists to prevent.
201
329
  */
202
- export declare function repairRegistration(settings: ClaudeSettings): boolean;
203
- /** The two project settings files the installer can write. */
204
- export declare function projectSettingsPaths(projectRoot: string): readonly string[];
330
+ export declare function repairRegistration(harness: HarnessRegistration, settings: ClaudeSettings): boolean;
205
331
  export declare function readSettings(settingsPath: string): ClaudeSettings;
206
332
  export declare function writeSettings(settingsPath: string, settings: ClaudeSettings): void;
207
- /** True when either project settings file under `root` carries a stale registration. */
208
- export declare function registrationStaleAt(root: string | null): boolean;
209
- /** True when either project settings file under `root` is missing the managed `env` entry. */
333
+ /**
334
+ * True when any settings file ONE HARNESS owns under `root` carries a stale registration.
335
+ *
336
+ * Per harness, not per repo, so the drift report can NAME which one moved — `.claude/settings.json` and
337
+ * `.codex/hooks.json` have different cures, and a fault that says only "the registration is stale" sends
338
+ * the reader to the wrong file half the time.
339
+ *
340
+ * A file that does not exist, or exists and registers no webpieces hooks, is never judged (see
341
+ * `registrationStale`). That is what keeps a repo which has never armed Codex from suddenly faulting on
342
+ * a `.codex/hooks.json` it does not have.
343
+ */
344
+ export declare function registrationStaleAt(harness: HarnessRegistration, root: string | null): boolean;
345
+ /**
346
+ * True when either Claude project settings file under `root` is missing the managed `env` entry.
347
+ *
348
+ * CLAUDE-ONLY by construction, and stated as such rather than looped over the harnesses: `env` is a
349
+ * Claude Code settings surface, Codex has no equivalent, and Codex's cwd is measured not to drift, which
350
+ * is the whole thing the entry is for. See HarnessRegistration.managesEnv.
351
+ */
210
352
  export declare function envStaleAt(root: string | null): boolean;
211
353
  /**
212
354
  * WHAT was rewritten in ONE settings file. Data-only → a class, per CLAUDE.md.
@@ -221,24 +363,39 @@ export declare class SettingsRepair {
221
363
  readonly env: boolean;
222
364
  constructor(settingsPath: string, registration: boolean, env: boolean);
223
365
  }
224
- /** Rewrite every stale project settings file under `root`; returns what changed, per file. */
366
+ /**
367
+ * Rewrite every stale settings file under `root`, for EVERY harness; returns what changed, per file.
368
+ *
369
+ * Existing files only — this never CREATES a registration. Arming a harness is the installer's decision
370
+ * (`wp-install-ai-hooks`); this is the cure for one that has already been armed and has drifted, so a
371
+ * repo that has never armed Codex is left exactly as it was.
372
+ */
225
373
  export declare function repairRegistrationAt(root: string): readonly SettingsRepair[];
226
374
  /**
227
- * The THREE names the drift check reports, so a deny can say WHICH of them moved.
375
+ * The names the drift check reports, so a deny can say WHICH surface moved.
376
+ *
377
+ * There were four, then three, and there are now FOUR again — but the fourth is not the one that was
378
+ * deleted. `GUARANTEE_ROOT_SURFACE` went with L-1 itself: an absolutely-registered shim resolves from
379
+ * any cwd, so there is no launch guarantee left to police and no second .sh file to keep byte-locked. A
380
+ * settings file still carrying the retired H1 entry is not its own surface — it is ordinary REGISTRATION
381
+ * drift, which `registrationStaleAt()` reports and `repairRegistration()` fixes by removing it.
382
+ *
383
+ * The new fourth is `.codex/hooks.json`, and it is here because of what happened while it was NOT a
384
+ * managed surface: something else wrote it, with a matcher that matched no Codex file tool and a shim
385
+ * path anchored on a variable Codex does not set, and no check in this package could see it. A file the
386
+ * guards depend on and nothing validates is the exact shape of that incident.
228
387
  *
229
- * There were four. `GUARANTEE_ROOT_SURFACE` is gone with L-1 itself: an absolutely-registered shim
230
- * resolves from any cwd, so there is no launch guarantee left to police and no second .sh file to keep
231
- * byte-locked. A settings file still carrying the retired H1 entry is not its own surface any more —
232
- * it is ordinary REGISTRATION drift, which `registrationStaleAt()` already reports and
233
- * `repairRegistration()` already fixes by removing it.
388
+ * Each harness's registration surface is its own name, from HarnessRegistration.registrationSurface, so
389
+ * the deny sends the reader to the file that actually moved.
234
390
  */
235
391
  export declare const SHIM_SURFACE = ".claude/webpieces/ai-hook.sh";
236
- export declare const REGISTRATION_SURFACE = ".claude/settings.json hook registration";
392
+ export declare const REGISTRATION_SURFACE: string;
393
+ export declare const CODEX_REGISTRATION_SURFACE: string;
237
394
  export declare const ENV_SURFACE = ".claude/settings.json env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR";
238
395
  /**
239
- * WHICH of the three managed surfaces disagree with this release — the input to fault S.
396
+ * WHICH of the managed surfaces disagree with this release — the input to fault S.
240
397
  *
241
- * All three are checked against the SAME root, resolved from the RUNNING MODULE (governingShimRoot),
398
+ * All of them are checked against the SAME root, resolved from the RUNNING MODULE (governingShimRoot),
242
399
  * never from cwd and never from `$CLAUDE_PROJECT_DIR`: the files we compare and the renderers we
243
400
  * compare them TO must come from one install, or the check straddles two trees and can never converge.
244
401
  */