@webpieces/ai-hook-rules 0.3.221 → 0.3.222

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/ai-hook-rules",
3
- "version": "0.3.221",
3
+ "version": "0.3.222",
4
4
  "description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -35,7 +35,7 @@
35
35
  "directory": "packages/tooling/ai-hook-rules"
36
36
  },
37
37
  "dependencies": {
38
- "@webpieces/rules-config": "0.3.221"
38
+ "@webpieces/rules-config": "0.3.222"
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
@@ -11,6 +11,7 @@ const load_config_1 = require("../core/load-config");
11
11
  const types_1 = require("../core/types");
12
12
  const to_error_1 = require("../core/to-error");
13
13
  const claude_code_response_1 = require("./claude-code-response");
14
+ const shim_1 = require("../bin/shim");
14
15
  const HANDLED_FILE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
15
16
  function readStdin() {
16
17
  return new Promise((resolve) => {
@@ -120,6 +121,12 @@ async function runMain(mode) {
120
121
  // process.cwd(); they match today, but the payload is the authoritative signal and stays
121
122
  // correct if the hook is ever invoked from a fixed dir (e.g. via $CLAUDE_PROJECT_DIR).
122
123
  const cwd = payload.cwd ?? process.cwd();
124
+ // Keep the committed shim (.claude/webpieces/ai-hook.sh) identical to renderShim() so its
125
+ // fail-closed escape hatch + installer allowlist never go stale — no human hand-edits it.
126
+ // Runs only when the guards binary is actually installed (i.e. now), is best-effort, and
127
+ // never throws into the decision below. 'rules' hook skips it (guards owns the shim).
128
+ if (mode !== 'rules')
129
+ (0, shim_1.healShim)(cwd);
123
130
  if (payload.tool_name === 'Bash') {
124
131
  // No code-style rule is bash-scoped, so the rules hook ignores Bash.
125
132
  if (mode === 'rules') {
@@ -1 +1 @@
1
- {"version":3,"file":"hook-core.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-core.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { run, runBash } from '../core/runner';\nimport { logRejection } from '../core/rejection-log';\nimport { logGuardDecision, GuardDecision, branchForLog } from '../core/decision-log';\nimport { triggerMainSyncRefresh } from '../core/main-sync-refresh';\nimport { CONFIG_FILENAME } from '../core/load-config';\nimport { NormalizedToolInput, NormalizedEdit, ToolKind, InformAiError, RuleFailError, HookMode } from '../core/types';\nimport { toError } from '../core/to-error';\nimport { emitDeny, emitAllow } from './claude-code-response';\n\n// Which category of rules this hook invocation runs. The hook is split into two independently\n// installable PreToolUse hooks; each runs ONE category (the runner filters by it), and both can\n// receive file AND bash payloads:\n// - 'rules' → code-style rules (file/edit scope). Bash payloads pass through (no code rules apply).\n// - 'guards' → hookGuards section: bash git/PR guards on Bash AND file guards (feature-branch-guard)\n// on Write/Edit. Matcher is Write|Edit|MultiEdit|Bash.\n// - 'all' → both categories, for the combined back-compat `wp-ai-hook` bin.\nexport type { HookMode };\n\nconst HANDLED_FILE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);\n\ninterface ClaudeCodePayload {\n tool_name: string;\n tool_input: ClaudeCodeToolInput;\n // Claude Code sends the session's current working directory (follows a persisted `cd`). Used to\n // scope guards to the git repo the AI is actually in — see runner git-repo-boundary governance.\n cwd?: string;\n}\n\ninterface ClaudeCodeToolInput {\n file_path?: string;\n content?: string;\n old_string?: string;\n new_string?: string;\n edits?: ClaudeCodeEditEntry[];\n command?: string;\n}\n\ninterface ClaudeCodeEditEntry {\n old_string?: string;\n new_string?: string;\n}\n\nfunction readStdin(): Promise<string> {\n return new Promise((resolve: (value: string) => void) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => { data += chunk; });\n process.stdin.on('end', () => resolve(data));\n process.stdin.on('error', () => resolve(''));\n if (process.stdin.isTTY) resolve('');\n });\n}\n\nfunction safeParse(raw: string): ClaudeCodePayload | null {\n if (!raw || raw.trim() === '') return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeCodePayload;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Malformed hook input from Claude Code stdin: ${error.message}`, { cause: error });\n }\n}\n\nfunction normalizeToolKind(toolName: string): ToolKind | null {\n if (HANDLED_FILE_TOOLS.has(toolName)) return toolName as ToolKind;\n return null;\n}\n\nfunction normalizeToolInput(toolKind: ToolKind, toolInput: ClaudeCodeToolInput): NormalizedToolInput | null {\n const filePath = toolInput.file_path;\n if (!filePath) return null;\n\n if (toolKind === 'Write') {\n return new NormalizedToolInput(filePath, [\n new NormalizedEdit('', toolInput.content || ''),\n ]);\n }\n if (toolKind === 'Edit') {\n return new NormalizedToolInput(filePath, [\n new NormalizedEdit(toolInput.old_string || '', toolInput.new_string || ''),\n ]);\n }\n if (toolKind === 'MultiEdit') {\n const raw = Array.isArray(toolInput.edits) ? toolInput.edits : [];\n const edits = raw.map((e: ClaudeCodeEditEntry) => new NormalizedEdit(e.old_string || '', e.new_string || ''));\n return new NormalizedToolInput(filePath, edits);\n }\n return null;\n}\n\nfunction handleBash(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n const command = payload.tool_input.command;\n if (!command || command.trim() === '') { emitAllow(); }\n const result = runBash(command, cwd, mode);\n if (!result) { emitAllow(); }\n emitDeny(result.report);\n}\n\nfunction handleFileTool(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n const toolKind = normalizeToolKind(payload.tool_name);\n if (!toolKind) { emitAllow(); }\n\n const input = normalizeToolInput(toolKind, payload.tool_input);\n if (!input) { emitAllow(); }\n\n // Always allow edits to webpieces.config.json — it's the fix target when the config is broken.\n // This exits BEFORE run(), so feature-branch-guard never sees a config edit; record that so the\n // audit trail explains why a config edit on a bad branch was not blocked (see decision-log.ts).\n if (path.basename(input.filePath) === CONFIG_FILENAME) {\n if (mode !== 'rules') {\n logGuardDecision(\n cwd,\n new GuardDecision('feature-branch-guard', toolKind, input.filePath, branchForLog(cwd), 'ALLOW', 'config-bypass (feature-branch-guard skipped)'),\n );\n // The guard's own refresh trigger lives inside its check(), which we skip here — so warm\n // the cache directly, otherwise a session that only edits webpieces.config.json never\n // refreshes the sync status. Fire-and-forget; never blocks the edit.\n triggerMainSyncRefresh(cwd);\n }\n emitAllow();\n }\n\n const result = run(toolKind, input, cwd, mode);\n if (!result) { emitAllow(); }\n\n logRejection(toolKind, input, result, cwd);\n emitDeny(result.report);\n}\n\n/**\n * Shared entry point for all three Claude Code PreToolUse adapters. `mode` selects which tool kinds\n * to validate; payloads outside the mode's scope pass through (emitAllow). Blocks by emitting a\n * PreToolUse `permissionDecision:\"deny\"` JSON on stdout (exit 0) — see claude-code-response.ts. Fails\n * CLOSED on any unexpected crash (emits a deny) so a broken hook never silently lets an edit through,\n * and the reason now surfaces in the Claude Code UI instead of being hidden on a stderr+exit-2 block.\n */\nexport async function runMain(mode: HookMode): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = await readStdin();\n const payload = safeParse(raw);\n if (!payload) { emitAllow(); }\n\n // Prefer the payload cwd (the AI's actual working dir, follows a persisted `cd`) over\n // process.cwd(); they match today, but the payload is the authoritative signal and stays\n // correct if the hook is ever invoked from a fixed dir (e.g. via $CLAUDE_PROJECT_DIR).\n const cwd = payload.cwd ?? process.cwd();\n\n if (payload.tool_name === 'Bash') {\n // No code-style rule is bash-scoped, so the rules hook ignores Bash.\n if (mode === 'rules') { emitAllow(); }\n handleBash(payload, cwd, mode);\n return;\n }\n\n // File payloads run in 'rules' (code-style), 'guards' (file-scoped guards like\n // feature-branch-guard), and 'all'. The runner filters to the right category.\n handleFileTool(payload, cwd, mode);\n } catch (err: unknown) {\n const error = toError(err);\n // An escaped RuleFailError (a rule that threw past the runner's per-rule catch) or an\n // InformAiError (bad config/stdin) both carry an AI-readable message; anything else is an\n // unexpected bug. All three deny (fail closed) and surface their reason to the AI.\n if (error instanceof RuleFailError) {\n emitDeny(error.aiMessage);\n } else if (error instanceof InformAiError) {\n emitDeny(error.message);\n } else {\n emitDeny(`[ai-hooks] hook crashed unexpectedly — failing closed: ${error.message}`);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"hook-core.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/adapters/hook-core.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { run, runBash } from '../core/runner';\nimport { logRejection } from '../core/rejection-log';\nimport { logGuardDecision, GuardDecision, branchForLog } from '../core/decision-log';\nimport { triggerMainSyncRefresh } from '../core/main-sync-refresh';\nimport { CONFIG_FILENAME } from '../core/load-config';\nimport { NormalizedToolInput, NormalizedEdit, ToolKind, InformAiError, RuleFailError, HookMode } from '../core/types';\nimport { toError } from '../core/to-error';\nimport { emitDeny, emitAllow } from './claude-code-response';\nimport { healShim } from '../bin/shim';\n\n// Which category of rules this hook invocation runs. The hook is split into two independently\n// installable PreToolUse hooks; each runs ONE category (the runner filters by it), and both can\n// receive file AND bash payloads:\n// - 'rules' → code-style rules (file/edit scope). Bash payloads pass through (no code rules apply).\n// - 'guards' → hookGuards section: bash git/PR guards on Bash AND file guards (feature-branch-guard)\n// on Write/Edit. Matcher is Write|Edit|MultiEdit|Bash.\n// - 'all' → both categories, for the combined back-compat `wp-ai-hook` bin.\nexport type { HookMode };\n\nconst HANDLED_FILE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);\n\ninterface ClaudeCodePayload {\n tool_name: string;\n tool_input: ClaudeCodeToolInput;\n // Claude Code sends the session's current working directory (follows a persisted `cd`). Used to\n // scope guards to the git repo the AI is actually in — see runner git-repo-boundary governance.\n cwd?: string;\n}\n\ninterface ClaudeCodeToolInput {\n file_path?: string;\n content?: string;\n old_string?: string;\n new_string?: string;\n edits?: ClaudeCodeEditEntry[];\n command?: string;\n}\n\ninterface ClaudeCodeEditEntry {\n old_string?: string;\n new_string?: string;\n}\n\nfunction readStdin(): Promise<string> {\n return new Promise((resolve: (value: string) => void) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk: string) => { data += chunk; });\n process.stdin.on('end', () => resolve(data));\n process.stdin.on('error', () => resolve(''));\n if (process.stdin.isTTY) resolve('');\n });\n}\n\nfunction safeParse(raw: string): ClaudeCodePayload | null {\n if (!raw || raw.trim() === '') return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeCodePayload;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Malformed hook input from Claude Code stdin: ${error.message}`, { cause: error });\n }\n}\n\nfunction normalizeToolKind(toolName: string): ToolKind | null {\n if (HANDLED_FILE_TOOLS.has(toolName)) return toolName as ToolKind;\n return null;\n}\n\nfunction normalizeToolInput(toolKind: ToolKind, toolInput: ClaudeCodeToolInput): NormalizedToolInput | null {\n const filePath = toolInput.file_path;\n if (!filePath) return null;\n\n if (toolKind === 'Write') {\n return new NormalizedToolInput(filePath, [\n new NormalizedEdit('', toolInput.content || ''),\n ]);\n }\n if (toolKind === 'Edit') {\n return new NormalizedToolInput(filePath, [\n new NormalizedEdit(toolInput.old_string || '', toolInput.new_string || ''),\n ]);\n }\n if (toolKind === 'MultiEdit') {\n const raw = Array.isArray(toolInput.edits) ? toolInput.edits : [];\n const edits = raw.map((e: ClaudeCodeEditEntry) => new NormalizedEdit(e.old_string || '', e.new_string || ''));\n return new NormalizedToolInput(filePath, edits);\n }\n return null;\n}\n\nfunction handleBash(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n const command = payload.tool_input.command;\n if (!command || command.trim() === '') { emitAllow(); }\n const result = runBash(command, cwd, mode);\n if (!result) { emitAllow(); }\n emitDeny(result.report);\n}\n\nfunction handleFileTool(payload: ClaudeCodePayload, cwd: string, mode: HookMode): void {\n const toolKind = normalizeToolKind(payload.tool_name);\n if (!toolKind) { emitAllow(); }\n\n const input = normalizeToolInput(toolKind, payload.tool_input);\n if (!input) { emitAllow(); }\n\n // Always allow edits to webpieces.config.json — it's the fix target when the config is broken.\n // This exits BEFORE run(), so feature-branch-guard never sees a config edit; record that so the\n // audit trail explains why a config edit on a bad branch was not blocked (see decision-log.ts).\n if (path.basename(input.filePath) === CONFIG_FILENAME) {\n if (mode !== 'rules') {\n logGuardDecision(\n cwd,\n new GuardDecision('feature-branch-guard', toolKind, input.filePath, branchForLog(cwd), 'ALLOW', 'config-bypass (feature-branch-guard skipped)'),\n );\n // The guard's own refresh trigger lives inside its check(), which we skip here — so warm\n // the cache directly, otherwise a session that only edits webpieces.config.json never\n // refreshes the sync status. Fire-and-forget; never blocks the edit.\n triggerMainSyncRefresh(cwd);\n }\n emitAllow();\n }\n\n const result = run(toolKind, input, cwd, mode);\n if (!result) { emitAllow(); }\n\n logRejection(toolKind, input, result, cwd);\n emitDeny(result.report);\n}\n\n/**\n * Shared entry point for all three Claude Code PreToolUse adapters. `mode` selects which tool kinds\n * to validate; payloads outside the mode's scope pass through (emitAllow). Blocks by emitting a\n * PreToolUse `permissionDecision:\"deny\"` JSON on stdout (exit 0) — see claude-code-response.ts. Fails\n * CLOSED on any unexpected crash (emits a deny) so a broken hook never silently lets an edit through,\n * and the reason now surfaces in the Claude Code UI instead of being hidden on a stderr+exit-2 block.\n */\nexport async function runMain(mode: HookMode): Promise<void> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = await readStdin();\n const payload = safeParse(raw);\n if (!payload) { emitAllow(); }\n\n // Prefer the payload cwd (the AI's actual working dir, follows a persisted `cd`) over\n // process.cwd(); they match today, but the payload is the authoritative signal and stays\n // correct if the hook is ever invoked from a fixed dir (e.g. via $CLAUDE_PROJECT_DIR).\n const cwd = payload.cwd ?? process.cwd();\n\n // Keep the committed shim (.claude/webpieces/ai-hook.sh) identical to renderShim() so its\n // fail-closed escape hatch + installer allowlist never go stale — no human hand-edits it.\n // Runs only when the guards binary is actually installed (i.e. now), is best-effort, and\n // never throws into the decision below. 'rules' hook skips it (guards owns the shim).\n if (mode !== 'rules') healShim(cwd);\n\n if (payload.tool_name === 'Bash') {\n // No code-style rule is bash-scoped, so the rules hook ignores Bash.\n if (mode === 'rules') { emitAllow(); }\n handleBash(payload, cwd, mode);\n return;\n }\n\n // File payloads run in 'rules' (code-style), 'guards' (file-scoped guards like\n // feature-branch-guard), and 'all'. The runner filters to the right category.\n handleFileTool(payload, cwd, mode);\n } catch (err: unknown) {\n const error = toError(err);\n // An escaped RuleFailError (a rule that threw past the runner's per-rule catch) or an\n // InformAiError (bad config/stdin) both carry an AI-readable message; anything else is an\n // unexpected bug. All three deny (fail closed) and surface their reason to the AI.\n if (error instanceof RuleFailError) {\n emitDeny(error.aiMessage);\n } else if (error instanceof InformAiError) {\n emitDeny(error.message);\n } else {\n emitDeny(`[ai-hooks] hook crashed unexpectedly — failing closed: ${error.message}`);\n }\n }\n}\n"]}
@@ -1,3 +1,5 @@
1
+ import { renderShim } from './shim';
2
+ export { renderShim };
1
3
  declare class HookSpec {
2
4
  readonly key: string;
3
5
  readonly label: string;
@@ -6,7 +8,6 @@ declare class HookSpec {
6
8
  constructor(key: string, label: string, matcher: string, bin: string);
7
9
  commandFor(target: InstallTarget, projectRoot: string): string;
8
10
  }
9
- export declare function renderShim(): string;
10
11
  export declare class InstallTarget {
11
12
  readonly choice: string;
12
13
  readonly label: string;
@@ -53,4 +54,3 @@ export declare function applyHook(hook: HookSpec, chosen: InstallTarget | null,
53
54
  export declare function resolveTargetChoice(name: string): string | null;
54
55
  export declare function parseTargetArg(args: string[]): string | null;
55
56
  export declare function main(): Promise<void>;
56
- export {};
package/src/bin/setup.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GUARDS_HOOK = exports.RULES_HOOK = exports.InstallTarget = void 0;
4
- exports.renderShim = renderShim;
3
+ exports.GUARDS_HOOK = exports.RULES_HOOK = exports.InstallTarget = exports.renderShim = void 0;
5
4
  exports.installTargets = installTargets;
6
5
  exports.migrate = migrate;
7
6
  exports.readSettings = readSettings;
@@ -17,6 +16,8 @@ const os_1 = require("os");
17
16
  const readline_1 = require("readline");
18
17
  const rules_config_1 = require("@webpieces/rules-config");
19
18
  const to_error_1 = require("../core/to-error");
19
+ const shim_1 = require("./shim");
20
+ Object.defineProperty(exports, "renderShim", { enumerable: true, get: function () { return shim_1.renderShim; } });
20
21
  const CONFIG_FILENAME = 'webpieces.config.json';
21
22
  const DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';
22
23
  const DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';
@@ -58,58 +59,27 @@ class HookSpec {
58
59
  // `sh: No such file or directory` on every Write/Edit/Bash tool call. The bin name rides along in
59
60
  // the command string, so `command.includes(bin)` still detects/uninstalls each hook (hasHook /
60
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.
61
64
  // ---------------------------------------------------------------------------
62
- const SHIM_MARKER = '.claude/webpieces/ai-hook.sh';
63
- function shimPath(projectRoot) {
64
- return path.join(projectRoot, '.claude', 'webpieces', 'ai-hook.sh');
65
- }
66
65
  function shimCommand(bin) {
67
- // $CLAUDE_PROJECT_DIR (exported to hooks by Claude Code) = the project root, so the shim resolves
68
- // from any cwd. Quoted to survive spaces in the path.
69
- return `"$CLAUDE_PROJECT_DIR/${SHIM_MARKER}" ${bin}`;
70
- }
71
- function renderShim() {
72
- return `#!/bin/sh
73
- # Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; re-running the installer
74
- # overwrites this file. Checked in on purpose so the hook has a stable, committed entry point even
75
- # when node_modules is absent. Safe to delete along with the matching .claude/settings.json entries
76
- # if you remove @webpieces/ai-hook-rules.
77
- #
78
- # Usage (wired into .claude/settings.json): "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" <bin-name>
79
- BIN_NAME="$1"
80
- shift
81
- # Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the
82
- # caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).
83
- ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
84
- BIN="$ROOT/node_modules/.bin/$BIN_NAME"
85
- if [ -x "$BIN" ]; then
86
- exec "$BIN" "$@" # exec preserves stdin — hooks receive the tool payload as JSON on stdin
87
- fi
88
- # Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run, so
89
- # FAIL CLOSED — deny the tool call. We deny via Claude Code's PreToolUse JSON protocol
90
- # (permissionDecision "deny" on stdout, then exit 0) rather than a bare "exit 2". BOTH block the call,
91
- # but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the
92
- # model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never
93
- # see the "run pnpm install" fix (they'd just see the tool's optimistic action summary). This still
94
- # fails closed: permissionDecision "deny" blocks the tool; it is not silently allowed like a plain
95
- # exit 0 would be. Tell the human to install; their own terminal 'pnpm install' does not go through
96
- # this hook. The reason is a single JSON string with no double-quotes/backslashes, so it stays valid
97
- # JSON after \${BIN_NAME} (always wp-ai-rules-hook / wp-ai-guards-hook) is substituted in.
98
- REASON="❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (\${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)"
99
- printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\\n' "\$REASON"
100
- exit 0 # decision is carried by permissionDecision "deny", not the exit code
101
- `;
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}`;
102
72
  }
103
73
  // Idempotent: re-running the installer overwrites the managed shim in place.
104
74
  function writeShim(projectRoot) {
105
- const target = shimPath(projectRoot);
75
+ const target = (0, shim_1.shimPath)(projectRoot);
106
76
  fs.mkdirSync(path.dirname(target), { recursive: true });
107
- fs.writeFileSync(target, renderShim(), { mode: 0o755 });
77
+ fs.writeFileSync(target, (0, shim_1.renderShim)(), { mode: 0o755 });
108
78
  // writeFileSync's mode is only applied when creating the file; force it on overwrite too.
109
79
  fs.chmodSync(target, 0o755);
110
80
  }
111
81
  function removeShim(projectRoot) {
112
- const target = shimPath(projectRoot);
82
+ const target = (0, shim_1.shimPath)(projectRoot);
113
83
  if (fs.existsSync(target))
114
84
  fs.rmSync(target);
115
85
  }
@@ -118,7 +88,7 @@ function removeShim(projectRoot) {
118
88
  function shimReferenced(targets) {
119
89
  return targets.some((t) => {
120
90
  const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];
121
- return entries.some((e) => e.hooks.some((h) => h.command.includes(SHIM_MARKER)));
91
+ return entries.some((e) => e.hooks.some((h) => h.command.includes(shim_1.SHIM_MARKER)));
122
92
  });
123
93
  }
124
94
  class InstallTarget {
@@ -1 +1 @@
1
- {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\nimport { createInterface } from 'readline';\n\nimport { allRuleNames, sectionForRule, isHookGuard } from '@webpieces/rules-config';\n\nimport { toError } from '../core/to-error';\n\nconst CONFIG_FILENAME = 'webpieces.config.json';\nconst DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';\nconst DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';\nconst DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';\n\n// ---------------------------------------------------------------------------\n// The two independently-installable hooks. Each can land in a different settings\n// file (see InstallTarget) so a team can ship the guards while a developer keeps\n// the code-style rules local while iterating.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly matcher: string,\n readonly bin: string,\n ) {}\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n // Project (relative) targets point at the checked-in shim via $CLAUDE_PROJECT_DIR (the project\n // root Claude Code exports to hooks). Using $CLAUDE_PROJECT_DIR — NOT a bare `./…` — means the\n // hook resolves from ANY cwd (a monorepo subdir, or a nested clone under repositories/) instead\n // of `command not found` (exit 127) silently skipping the guard. It stays portable (no hardcoded\n // absolute path), and the shim still degrades gracefully when node_modules is absent. See\n // writeShim(); the git-repo-boundary decision (foreign clone → allow) then happens in the binary.\n commandFor(target: InstallTarget, projectRoot: string): string {\n if (target.absolute) {\n return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;\n }\n return shimCommand(this.bin);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Single checked-in shim (.claude/webpieces/ai-hook.sh). Both project hooks point at it, passing\n// their bin name as the first arg. settings.json points here (not at the bare bin) so a missing bin\n// (fresh clone, package removed) yields a friendly \"run pnpm install\" line instead of the raw\n// `sh: No such file or directory` on every Write/Edit/Bash tool call. The bin name rides along in\n// the command string, so `command.includes(bin)` still detects/uninstalls each hook (hasHook /\n// removeHook). `.claude` is committed, so the shim survives even when node_modules does not.\n// ---------------------------------------------------------------------------\nconst SHIM_MARKER = '.claude/webpieces/ai-hook.sh';\n\nfunction shimPath(projectRoot: string): string {\n return path.join(projectRoot, '.claude', 'webpieces', 'ai-hook.sh');\n}\n\nfunction shimCommand(bin: string): string {\n // $CLAUDE_PROJECT_DIR (exported to hooks by Claude Code) = the project root, so the shim resolves\n // from any cwd. Quoted to survive spaces in the path.\n return `\"$CLAUDE_PROJECT_DIR/${SHIM_MARKER}\" ${bin}`;\n}\n\nexport function renderShim(): string {\n return `#!/bin/sh\n# Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; re-running the installer\n# overwrites this file. Checked in on purpose so the hook has a stable, committed entry point even\n# when node_modules is absent. Safe to delete along with the matching .claude/settings.json entries\n# if you remove @webpieces/ai-hook-rules.\n#\n# Usage (wired into .claude/settings.json): \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" <bin-name>\nBIN_NAME=\"$1\"\nshift\n# Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the\n# caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).\nROOT=\"$(CDPATH= cd -- \"$(dirname -- \"$0\")/../..\" && pwd)\"\nBIN=\"$ROOT/node_modules/.bin/$BIN_NAME\"\nif [ -x \"$BIN\" ]; then\n exec \"$BIN\" \"$@\" # exec preserves stdin — hooks receive the tool payload as JSON on stdin\nfi\n# Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run, so\n# FAIL CLOSED — deny the tool call. We deny via Claude Code's PreToolUse JSON protocol\n# (permissionDecision \"deny\" on stdout, then exit 0) rather than a bare \"exit 2\". BOTH block the call,\n# but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the\n# model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never\n# see the \"run pnpm install\" fix (they'd just see the tool's optimistic action summary). This still\n# fails closed: permissionDecision \"deny\" blocks the tool; it is not silently allowed like a plain\n# exit 0 would be. Tell the human to install; their own terminal 'pnpm install' does not go through\n# this hook. The reason is a single JSON string with no double-quotes/backslashes, so it stays valid\n# JSON after \\${BIN_NAME} (always wp-ai-rules-hook / wp-ai-guards-hook) is substituted in.\nREASON=\"❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (\\${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)\"\nprintf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"%s\"}}\\\\n' \"\\$REASON\"\nexit 0 # decision is carried by permissionDecision \"deny\", not the exit code\n`;\n}\n\n// Idempotent: re-running the installer overwrites the managed shim in place.\nfunction writeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode is only applied when creating the file; force it on overwrite too.\n fs.chmodSync(target, 0o755);\n}\n\nfunction removeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n if (fs.existsSync(target)) fs.rmSync(target);\n}\n\n// The shim is shared by both hooks — only safe to delete once no project settings file references\n// it anymore (i.e. the other hook was moved to global or uninstalled too).\nfunction shimReferenced(targets: InstallTarget[]): boolean {\n return targets.some((t: InstallTarget) => {\n const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(SHIM_MARKER)));\n });\n}\n\nexport class InstallTarget {\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n ) {}\n}\n\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', 'Write|Edit|MultiEdit', 'wp-ai-rules-hook');\n// Guards match BOTH Bash (git/PR guards) and Write|Edit|MultiEdit (file-scoped guards like\n// feature-branch-guard), so ALL guards run in the guards hook regardless of scope.\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', 'Write|Edit|MultiEdit|Bash', 'wp-ai-guards-hook');\n\n// `homeDir` is injectable so tests can point the global target at a temp dir instead of the real\n// ~/.claude/settings.json (a unit test must never write the user's actual global settings).\nexport function installTargets(projectRoot: string, homeDir: string = homedir()): InstallTarget[] {\n return [\n new InstallTarget('1', 'project (.claude/settings.json — committed, for the team)',\n path.join(projectRoot, '.claude', 'settings.json'), false),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// webpieces.config.json seeding + migration to the rules / hookGuards / commands layout.\n// ---------------------------------------------------------------------------\n// webpieces-disable no-any-unknown -- webpieces.config.json / settings.json are opaque consumer JSON\ntype Json = Record<string, unknown>;\ntype RuleEntry = Json;\ntype Section = Record<string, RuleEntry>;\n\ninterface ConfigFile {\n extends?: string;\n rules: Section;\n hookGuards: Section;\n commands: Json;\n excludePaths: Json;\n rulesDir: string[];\n}\n\ninterface MigrateResult {\n config: ConfigFile;\n changes: string[];\n}\n\nfunction seedRule(): RuleEntry {\n return { mode: 'OFF', ignoreModifiedUntilEpoch: 0 };\n}\n\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n upsertPr: DEFAULT_UPSERT_PR,\n mergeComplete: DEFAULT_MERGE_COMPLETE,\n };\n}\n\n// Required excludePaths block: two glob lists that suppress hook enforcement per file path. Seeded\n// empty (enforce everywhere) — a client adds paths (e.g. \"repositories/**\") to exempt vendored trees.\nfunction seedExcludePaths(): Json {\n return { rules: [], guards: [] };\n}\n\nfunction buildSeedConfig(): ConfigFile {\n const rules: Section = {};\n const hookGuards: Section = {};\n for (const name of allRuleNames()) {\n if (sectionForRule(name) === 'hookGuards') hookGuards[name] = seedRule();\n else rules[name] = seedRule();\n }\n return { rules, hookGuards, commands: seedCommands(), excludePaths: seedExcludePaths(), rulesDir: [] };\n}\n\nfunction writeConfig(configPath: string, config: ConfigFile): void {\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n}\n\nfunction readConfig(configPath: string): Json {\n const raw = fs.readFileSync(configPath, 'utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as Json;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction asSection(value: Json[string]): Section {\n return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? (value as Section) : {};\n}\n\n// Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.\n// Returns a human-readable list of what changed (empty = already up to date).\nexport function migrate(existing: Json): MigrateResult {\n const changes: string[] = [];\n const rules: Section = asSection(existing['rules']);\n const hookGuards: Section = asSection(existing['hookGuards']);\n const commands: Json = (typeof existing['commands'] === 'object' && existing['commands'] !== null)\n ? (existing['commands'] as Json) : {};\n\n // Move a deprecated top-level pr-gate block under commands.\n if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {\n commands['pr-gate'] = existing['pr-gate'];\n changes.push('moved top-level \"pr-gate\" → commands[\"pr-gate\"]');\n }\n // Move guards mistakenly left in rules into hookGuards.\n for (const name of Object.keys(rules)) {\n if (isHookGuard(name)) {\n hookGuards[name] = rules[name];\n delete rules[name];\n changes.push(`moved \"${name}\" from rules → hookGuards`);\n }\n }\n // Move code rules mistakenly placed in hookGuards back into rules.\n for (const name of Object.keys(hookGuards)) {\n if (!isHookGuard(name) && allRuleNames().includes(name)) {\n rules[name] = hookGuards[name];\n delete hookGuards[name];\n changes.push(`moved \"${name}\" from hookGuards → rules`);\n }\n }\n // Add any missing built-in into its correct section (OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n target[name] = seedRule();\n changes.push(`added \"${name}\" (OFF) to ${sectionForRule(name)}`);\n }\n }\n // Fill command defaults.\n if (commands['pr-gate'] === undefined) {\n commands['pr-gate'] = { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] };\n changes.push('added commands[\"pr-gate\"] (OFF)');\n }\n if (commands['upsertPr'] === undefined) { commands['upsertPr'] = DEFAULT_UPSERT_PR; changes.push('added commands.upsertPr'); }\n if (commands['mergeComplete'] === undefined) { commands['mergeComplete'] = DEFAULT_MERGE_COMPLETE; changes.push('added commands.mergeComplete'); }\n\n // Seed the now-required excludePaths block (empty = enforce everywhere) if the config predates it.\n const excludePaths: Json = (typeof existing['excludePaths'] === 'object' && existing['excludePaths'] !== null && !Array.isArray(existing['excludePaths']))\n ? (existing['excludePaths'] as Json) : {};\n if (excludePaths['rules'] === undefined) { excludePaths['rules'] = []; changes.push('added excludePaths.rules ([])'); }\n if (excludePaths['guards'] === undefined) { excludePaths['guards'] = []; changes.push('added excludePaths.guards ([])'); }\n\n const rulesDir: string[] = Array.isArray(existing['rulesDir']) ? (existing['rulesDir'] as string[]) : [];\n const config: ConfigFile = { rules, hookGuards, commands, excludePaths, rulesDir };\n if (typeof existing['extends'] === 'string') config.extends = existing['extends'];\n return { config, changes };\n}\n\nfunction seedOrSyncConfig(projectRoot: string, syncOnly: boolean): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n if (syncOnly) {\n console.log(` [ai-hooks] No ${CONFIG_FILENAME} found — nothing to sync.`);\n return;\n }\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands), all rules OFF.`);\n console.log(' Enable the ones you want by changing \"mode\".');\n return;\n }\n const result = migrate(readConfig(configPath));\n if (result.changes.length === 0) {\n console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);\n return;\n }\n writeConfig(configPath, result.config);\n console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);\n for (const change of result.changes) console.log(` - ${change}`);\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code settings.json hook wiring.\n// ---------------------------------------------------------------------------\ninterface HookCommand { type: string; command: string; }\ninterface HookEntry { matcher: string; hooks: HookCommand[]; }\ninterface ClaudeSettings {\n hooks?: { PreToolUse?: HookEntry[] };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\nexport function hasHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(bin)));\n}\n\n// Drop every PreToolUse command referencing `bin`; returns true if anything was removed.\nfunction removeHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand) => !h.command.includes(bin));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\nfunction addHook(settings: ClaudeSettings, matcher: string, command: string): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher, hooks: [{ type: 'command', command }] });\n}\n\n// Apply the chosen install for one hook: remove it from every target file, then add it back to the\n// chosen one (or nowhere, for uninstall). Writes only the files that changed.\nexport function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void {\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n const removed = removeHook(settings, hook.bin);\n const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;\n if (isChosen) {\n addHook(settings, hook.matcher, hook.commandFor(target, projectRoot));\n writeSettings(target.settingsPath, settings);\n console.log(` ✅ ${hook.label} → ${target.label}`);\n } else if (removed) {\n writeSettings(target.settingsPath, settings);\n }\n }\n // Manage the shared checked-in shim: (re)write it whenever a project (relative) install exists,\n // otherwise clean it up once neither hook references it anymore.\n if (chosen !== null && !chosen.absolute) {\n writeShim(projectRoot);\n } else if (!shimReferenced(targets)) {\n removeShim(projectRoot);\n }\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\n}\n\nfunction currentLocation(hook: HookSpec, targets: InstallTarget[]): string {\n const here = targets.filter((t: InstallTarget) => hasHook(readSettings(t.settingsPath), hook.bin));\n return here.length === 0 ? 'none' : here.map((t: InstallTarget) => t.label.split(' (')[0]).join(', ');\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve: (answer: string) => void) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer: string) => { rl.close(); resolve(answer.trim()); });\n });\n}\n\n// Map a friendly `--target` name to an InstallTarget choice id (see installTargets). Returns null\n// for an unknown name so the caller can error out. Kept separate + exported for unit testing.\nexport function resolveTargetChoice(name: string): string | null {\n switch (name) {\n case 'project': return '1';\n case 'project-personal':\n case 'projectpersonal':\n case 'local': return '2';\n case 'global': return '3';\n case 'none':\n case 'uninstall': return '4';\n default: return null;\n }\n}\n\n// Extract the value of `--target=<name>` from argv (null if the flag is absent).\nexport function parseTargetArg(args: string[]): string | null {\n const flag = args.find((a: string): boolean => a.startsWith('--target='));\n return flag ? flag.slice('--target='.length) : null;\n}\n\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label} [matcher: ${hook.matcher}]`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n for (const target of targets) console.log(` ${target.choice}) ${target.label}`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n const chosen = targets.find((t: InstallTarget) => t.choice === answer) ?? null;\n applyHook(hook, chosen, targets, projectRoot);\n}\n\nexport async function main(): Promise<void> {\n const args = process.argv.slice(2);\n const syncOnly = args.includes('--sync');\n const projectRoot = process.cwd();\n\n seedOrSyncConfig(projectRoot, syncOnly);\n if (syncOnly) return;\n\n const targets = installTargets(projectRoot);\n\n // Non-interactive: `--target=project|project-personal|global|none` installs BOTH hooks at that\n // location without prompting, so an agent or CI can run the installer unattended (e.g. after a\n // @webpieces upgrade that changed the hook entry). Omit the flag for the interactive per-hook chooser.\n const targetName = parseTargetArg(args);\n if (targetName !== null) {\n const choice = resolveTargetChoice(targetName);\n if (choice === null) {\n console.error(`❌ Unknown --target '${targetName}'. Use one of: project | project-personal | global | none`);\n process.exitCode = 1;\n return;\n }\n const chosen = targets.find((t: InstallTarget): boolean => t.choice === choice) ?? null;\n applyHook(RULES_HOOK, chosen, targets, projectRoot);\n applyHook(GUARDS_HOOK, chosen, targets, projectRoot);\n console.log(`\\nDone. Both hooks set to: ${targetName}.`);\n return;\n }\n\n console.log('');\n console.log('Two webpieces hooks can be installed independently — choose a location for each:');\n await wireHook(RULES_HOOK, targets, projectRoot);\n await wireHook(GUARDS_HOOK, targets, projectRoot);\n console.log('');\n console.log('Done. Re-run wp-setup-ai-hooks anytime to move or uninstall a hook.');\n console.log('(Non-interactive: wp-setup-ai-hooks --target=project|project-personal|global|none)');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\nimport { createInterface } from 'readline';\n\nimport { allRuleNames, sectionForRule, isHookGuard } from '@webpieces/rules-config';\n\nimport { toError } from '../core/to-error';\nimport { SHIM_MARKER, shimPath, renderShim } from './shim';\n\n// Re-exported for back-compat (setup.spec.ts + external callers). The shim body + path now live in\n// ./shim (shared with the runtime self-heal in hook-core). See shim.ts for the single source of truth.\nexport { renderShim };\n\nconst CONFIG_FILENAME = 'webpieces.config.json';\nconst DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=origin/main';\nconst DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';\nconst DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';\n\n// ---------------------------------------------------------------------------\n// The two independently-installable hooks. Each can land in a different settings\n// file (see InstallTarget) so a team can ship the guards while a developer keeps\n// the code-style rules local while iterating.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly matcher: string,\n readonly bin: string,\n ) {}\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n // Project (relative) targets point at the checked-in shim via $CLAUDE_PROJECT_DIR (the project\n // root Claude Code exports to hooks). Using $CLAUDE_PROJECT_DIR — NOT a bare `./…` — means the\n // hook resolves from ANY cwd (a monorepo subdir, or a nested clone under repositories/) instead\n // of `command not found` (exit 127) silently skipping the guard. It stays portable (no hardcoded\n // absolute path), and the shim still degrades gracefully when node_modules is absent. See\n // writeShim(); the git-repo-boundary decision (foreign clone → allow) then happens in the binary.\n commandFor(target: InstallTarget, projectRoot: string): string {\n if (target.absolute) {\n return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;\n }\n return shimCommand(this.bin);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Single checked-in shim (.claude/webpieces/ai-hook.sh). Both project hooks point at it, passing\n// their bin name as the first arg. settings.json points here (not at the bare bin) so a missing bin\n// (fresh clone, package removed) yields a friendly \"run pnpm install\" line instead of the raw\n// `sh: No such file or directory` on every Write/Edit/Bash tool call. The bin name rides along in\n// the command string, so `command.includes(bin)` still detects/uninstalls each hook (hasHook /\n// removeHook). `.claude` is committed, so the shim survives even when node_modules does not.\n// The shim body + path live in ./shim (shared with the runtime self-heal in hook-core); only the\n// settings.json command string is built here.\n// ---------------------------------------------------------------------------\nfunction shimCommand(bin: string): string {\n // Invoke via `sh <file>` rather than executing the shim directly: `sh` reads a 0644 file fine, so\n // a missing executable bit on the checked-in shim (fresh clone, a filesystem that drops the bit,\n // git core.fileMode quirks) can NEVER break the hook with a raw `Permission denied` on every tool\n // call. $CLAUDE_PROJECT_DIR (exported to hooks by Claude Code) = the project root, so the shim\n // resolves from any cwd. Quoted to survive spaces in the path.\n return `sh \"$CLAUDE_PROJECT_DIR/${SHIM_MARKER}\" ${bin}`;\n}\n\n// Idempotent: re-running the installer overwrites the managed shim in place.\nfunction writeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode is only applied when creating the file; force it on overwrite too.\n fs.chmodSync(target, 0o755);\n}\n\nfunction removeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n if (fs.existsSync(target)) fs.rmSync(target);\n}\n\n// The shim is shared by both hooks — only safe to delete once no project settings file references\n// it anymore (i.e. the other hook was moved to global or uninstalled too).\nfunction shimReferenced(targets: InstallTarget[]): boolean {\n return targets.some((t: InstallTarget) => {\n const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(SHIM_MARKER)));\n });\n}\n\nexport class InstallTarget {\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n ) {}\n}\n\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', 'Write|Edit|MultiEdit', 'wp-ai-rules-hook');\n// Guards match BOTH Bash (git/PR guards) and Write|Edit|MultiEdit (file-scoped guards like\n// feature-branch-guard), so ALL guards run in the guards hook regardless of scope.\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', 'Write|Edit|MultiEdit|Bash', 'wp-ai-guards-hook');\n\n// `homeDir` is injectable so tests can point the global target at a temp dir instead of the real\n// ~/.claude/settings.json (a unit test must never write the user's actual global settings).\nexport function installTargets(projectRoot: string, homeDir: string = homedir()): InstallTarget[] {\n return [\n new InstallTarget('1', 'project (.claude/settings.json — committed, for the team)',\n path.join(projectRoot, '.claude', 'settings.json'), false),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// webpieces.config.json seeding + migration to the rules / hookGuards / commands layout.\n// ---------------------------------------------------------------------------\n// webpieces-disable no-any-unknown -- webpieces.config.json / settings.json are opaque consumer JSON\ntype Json = Record<string, unknown>;\ntype RuleEntry = Json;\ntype Section = Record<string, RuleEntry>;\n\ninterface ConfigFile {\n extends?: string;\n rules: Section;\n hookGuards: Section;\n commands: Json;\n excludePaths: Json;\n rulesDir: string[];\n}\n\ninterface MigrateResult {\n config: ConfigFile;\n changes: string[];\n}\n\nfunction seedRule(): RuleEntry {\n return { mode: 'OFF', ignoreModifiedUntilEpoch: 0 };\n}\n\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n upsertPr: DEFAULT_UPSERT_PR,\n mergeComplete: DEFAULT_MERGE_COMPLETE,\n };\n}\n\n// Required excludePaths block: two glob lists that suppress hook enforcement per file path. Seeded\n// empty (enforce everywhere) — a client adds paths (e.g. \"repositories/**\") to exempt vendored trees.\nfunction seedExcludePaths(): Json {\n return { rules: [], guards: [] };\n}\n\nfunction buildSeedConfig(): ConfigFile {\n const rules: Section = {};\n const hookGuards: Section = {};\n for (const name of allRuleNames()) {\n if (sectionForRule(name) === 'hookGuards') hookGuards[name] = seedRule();\n else rules[name] = seedRule();\n }\n return { rules, hookGuards, commands: seedCommands(), excludePaths: seedExcludePaths(), rulesDir: [] };\n}\n\nfunction writeConfig(configPath: string, config: ConfigFile): void {\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n}\n\nfunction readConfig(configPath: string): Json {\n const raw = fs.readFileSync(configPath, 'utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as Json;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction asSection(value: Json[string]): Section {\n return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? (value as Section) : {};\n}\n\n// Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.\n// Returns a human-readable list of what changed (empty = already up to date).\nexport function migrate(existing: Json): MigrateResult {\n const changes: string[] = [];\n const rules: Section = asSection(existing['rules']);\n const hookGuards: Section = asSection(existing['hookGuards']);\n const commands: Json = (typeof existing['commands'] === 'object' && existing['commands'] !== null)\n ? (existing['commands'] as Json) : {};\n\n // Move a deprecated top-level pr-gate block under commands.\n if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {\n commands['pr-gate'] = existing['pr-gate'];\n changes.push('moved top-level \"pr-gate\" → commands[\"pr-gate\"]');\n }\n // Move guards mistakenly left in rules into hookGuards.\n for (const name of Object.keys(rules)) {\n if (isHookGuard(name)) {\n hookGuards[name] = rules[name];\n delete rules[name];\n changes.push(`moved \"${name}\" from rules → hookGuards`);\n }\n }\n // Move code rules mistakenly placed in hookGuards back into rules.\n for (const name of Object.keys(hookGuards)) {\n if (!isHookGuard(name) && allRuleNames().includes(name)) {\n rules[name] = hookGuards[name];\n delete hookGuards[name];\n changes.push(`moved \"${name}\" from hookGuards → rules`);\n }\n }\n // Add any missing built-in into its correct section (OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n target[name] = seedRule();\n changes.push(`added \"${name}\" (OFF) to ${sectionForRule(name)}`);\n }\n }\n // Fill command defaults.\n if (commands['pr-gate'] === undefined) {\n commands['pr-gate'] = { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] };\n changes.push('added commands[\"pr-gate\"] (OFF)');\n }\n if (commands['upsertPr'] === undefined) { commands['upsertPr'] = DEFAULT_UPSERT_PR; changes.push('added commands.upsertPr'); }\n if (commands['mergeComplete'] === undefined) { commands['mergeComplete'] = DEFAULT_MERGE_COMPLETE; changes.push('added commands.mergeComplete'); }\n\n // Seed the now-required excludePaths block (empty = enforce everywhere) if the config predates it.\n const excludePaths: Json = (typeof existing['excludePaths'] === 'object' && existing['excludePaths'] !== null && !Array.isArray(existing['excludePaths']))\n ? (existing['excludePaths'] as Json) : {};\n if (excludePaths['rules'] === undefined) { excludePaths['rules'] = []; changes.push('added excludePaths.rules ([])'); }\n if (excludePaths['guards'] === undefined) { excludePaths['guards'] = []; changes.push('added excludePaths.guards ([])'); }\n\n const rulesDir: string[] = Array.isArray(existing['rulesDir']) ? (existing['rulesDir'] as string[]) : [];\n const config: ConfigFile = { rules, hookGuards, commands, excludePaths, rulesDir };\n if (typeof existing['extends'] === 'string') config.extends = existing['extends'];\n return { config, changes };\n}\n\nfunction seedOrSyncConfig(projectRoot: string, syncOnly: boolean): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n if (syncOnly) {\n console.log(` [ai-hooks] No ${CONFIG_FILENAME} found — nothing to sync.`);\n return;\n }\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands), all rules OFF.`);\n console.log(' Enable the ones you want by changing \"mode\".');\n return;\n }\n const result = migrate(readConfig(configPath));\n if (result.changes.length === 0) {\n console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);\n return;\n }\n writeConfig(configPath, result.config);\n console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);\n for (const change of result.changes) console.log(` - ${change}`);\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code settings.json hook wiring.\n// ---------------------------------------------------------------------------\ninterface HookCommand { type: string; command: string; }\ninterface HookEntry { matcher: string; hooks: HookCommand[]; }\ninterface ClaudeSettings {\n hooks?: { PreToolUse?: HookEntry[] };\n // webpieces-disable no-any-unknown -- opaque settings bag; arbitrary keys allowed\n [key: string]: unknown;\n}\n\nexport function readSettings(settingsPath: string): ClaudeSettings {\n if (!fs.existsSync(settingsPath)) return {};\n const raw = fs.readFileSync(settingsPath, 'utf8');\n if (raw.trim() === '') return {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as ClaudeSettings;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${settingsPath} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction writeSettings(settingsPath: string, settings: ClaudeSettings): void {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true });\n fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\\n');\n}\n\nexport function hasHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(bin)));\n}\n\n// Drop every PreToolUse command referencing `bin`; returns true if anything was removed.\nfunction removeHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand) => !h.command.includes(bin));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\nfunction addHook(settings: ClaudeSettings, matcher: string, command: string): void {\n if (!settings.hooks) settings.hooks = {};\n if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];\n settings.hooks.PreToolUse.push({ matcher, hooks: [{ type: 'command', command }] });\n}\n\n// Apply the chosen install for one hook: remove it from every target file, then add it back to the\n// chosen one (or nowhere, for uninstall). Writes only the files that changed.\nexport function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void {\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n const removed = removeHook(settings, hook.bin);\n const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;\n if (isChosen) {\n addHook(settings, hook.matcher, hook.commandFor(target, projectRoot));\n writeSettings(target.settingsPath, settings);\n console.log(` ✅ ${hook.label} → ${target.label}`);\n } else if (removed) {\n writeSettings(target.settingsPath, settings);\n }\n }\n // Manage the shared checked-in shim: (re)write it whenever a project (relative) install exists,\n // otherwise clean it up once neither hook references it anymore.\n if (chosen !== null && !chosen.absolute) {\n writeShim(projectRoot);\n } else if (!shimReferenced(targets)) {\n removeShim(projectRoot);\n }\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\n}\n\nfunction currentLocation(hook: HookSpec, targets: InstallTarget[]): string {\n const here = targets.filter((t: InstallTarget) => hasHook(readSettings(t.settingsPath), hook.bin));\n return here.length === 0 ? 'none' : here.map((t: InstallTarget) => t.label.split(' (')[0]).join(', ');\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve: (answer: string) => void) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer: string) => { rl.close(); resolve(answer.trim()); });\n });\n}\n\n// Map a friendly `--target` name to an InstallTarget choice id (see installTargets). Returns null\n// for an unknown name so the caller can error out. Kept separate + exported for unit testing.\nexport function resolveTargetChoice(name: string): string | null {\n switch (name) {\n case 'project': return '1';\n case 'project-personal':\n case 'projectpersonal':\n case 'local': return '2';\n case 'global': return '3';\n case 'none':\n case 'uninstall': return '4';\n default: return null;\n }\n}\n\n// Extract the value of `--target=<name>` from argv (null if the flag is absent).\nexport function parseTargetArg(args: string[]): string | null {\n const flag = args.find((a: string): boolean => a.startsWith('--target='));\n return flag ? flag.slice('--target='.length) : null;\n}\n\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label} [matcher: ${hook.matcher}]`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n for (const target of targets) console.log(` ${target.choice}) ${target.label}`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n const chosen = targets.find((t: InstallTarget) => t.choice === answer) ?? null;\n applyHook(hook, chosen, targets, projectRoot);\n}\n\nexport async function main(): Promise<void> {\n const args = process.argv.slice(2);\n const syncOnly = args.includes('--sync');\n const projectRoot = process.cwd();\n\n seedOrSyncConfig(projectRoot, syncOnly);\n if (syncOnly) return;\n\n const targets = installTargets(projectRoot);\n\n // Non-interactive: `--target=project|project-personal|global|none` installs BOTH hooks at that\n // location without prompting, so an agent or CI can run the installer unattended (e.g. after a\n // @webpieces upgrade that changed the hook entry). Omit the flag for the interactive per-hook chooser.\n const targetName = parseTargetArg(args);\n if (targetName !== null) {\n const choice = resolveTargetChoice(targetName);\n if (choice === null) {\n console.error(`❌ Unknown --target '${targetName}'. Use one of: project | project-personal | global | none`);\n process.exitCode = 1;\n return;\n }\n const chosen = targets.find((t: InstallTarget): boolean => t.choice === choice) ?? null;\n applyHook(RULES_HOOK, chosen, targets, projectRoot);\n applyHook(GUARDS_HOOK, chosen, targets, projectRoot);\n console.log(`\\nDone. Both hooks set to: ${targetName}.`);\n return;\n }\n\n console.log('');\n console.log('Two webpieces hooks can be installed independently — choose a location for each:');\n await wireHook(RULES_HOOK, targets, projectRoot);\n await wireHook(GUARDS_HOOK, targets, projectRoot);\n console.log('');\n console.log('Done. Re-run wp-setup-ai-hooks anytime to move or uninstall a hook.');\n console.log('(Non-interactive: wp-setup-ai-hooks --target=project|project-personal|global|none)');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
@@ -0,0 +1,6 @@
1
+ export declare const SHIM_MARKER = ".claude/webpieces/ai-hook.sh";
2
+ export declare function shimPath(projectRoot: string): string;
3
+ export declare const INSTALLER_ALLOW_ERE = "^(pnpm|npm) install([[:space:]]+--[A-Za-z][A-Za-z-]*)*[[:space:]]*$";
4
+ export declare const INSTALLER_ALLOW_JS: RegExp;
5
+ export declare function renderShim(): string;
6
+ export declare function healShim(cwd: string): void;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INSTALLER_ALLOW_JS = exports.INSTALLER_ALLOW_ERE = exports.SHIM_MARKER = void 0;
4
+ exports.shimPath = shimPath;
5
+ exports.renderShim = renderShim;
6
+ exports.healShim = healShim;
7
+ const tslib_1 = require("tslib");
8
+ const fs = tslib_1.__importStar(require("fs"));
9
+ const path = tslib_1.__importStar(require("path"));
10
+ // ---------------------------------------------------------------------------
11
+ // The single checked-in shim (.claude/webpieces/ai-hook.sh). Both project hooks point at it, passing
12
+ // their bin name as the first arg. settings.json points here (not at the bare bin) so a missing bin
13
+ // (fresh clone, package removed) yields a friendly message instead of the raw `sh: No such file or
14
+ // directory` on every Write/Edit/Bash tool call. `.claude` is committed, so the shim survives even
15
+ // when node_modules does not.
16
+ //
17
+ // This module is the SINGLE SOURCE OF TRUTH for the shim body + the installer allowlist. The
18
+ // installer (setup.ts) renders it on install; the running guards binary re-renders and self-heals it
19
+ // (healShim) so the committed .sh can never go stale — no human ever hand-edits it.
20
+ // ---------------------------------------------------------------------------
21
+ exports.SHIM_MARKER = '.claude/webpieces/ai-hook.sh';
22
+ function shimPath(projectRoot) {
23
+ return path.join(projectRoot, '.claude', 'webpieces', 'ai-hook.sh');
24
+ }
25
+ // Package-manager install commands allowed to pass the fail-closed shim so the assistant can
26
+ // self-heal the guards (run `pnpm install`) when node_modules is absent — otherwise the guard blocks
27
+ // the very command that re-enables it (deadlock). nx/pnpm monorepo only. POSIX ERE (fed to `grep -E`).
28
+ //
29
+ // Base command + `--flags` only. Because nothing but `--word` tokens may follow `install`, shell
30
+ // operators (`;`, `&&`, `|`, backticks, `$()`, `>`, `<`) cannot match — so nothing can be smuggled
31
+ // alongside the install. Keep in sync with INSTALLER_ALLOW_JS below (locked by a unit test).
32
+ exports.INSTALLER_ALLOW_ERE = '^(pnpm|npm) install([[:space:]]+--[A-Za-z][A-Za-z-]*)*[[:space:]]*$';
33
+ // JS-regex twin of INSTALLER_ALLOW_ERE (POSIX `[[:space:]]` → `\s`). Not used by the fail-closed
34
+ // shim (which is pure sh), but kept as the single JS-side definition should a future guard ever need
35
+ // to recognise installer commands in the runner. A unit test asserts the two agree on a sample set.
36
+ exports.INSTALLER_ALLOW_JS = /^(pnpm|npm) install(\s+--[A-Za-z][A-Za-z-]*)*\s*$/;
37
+ // Normal template literal (not String.raw): it carries #235's shell escapes verbatim (\${BIN_NAME},
38
+ // \$REASON, \\n for the deny JSON) AND my sed backslashes (doubled: \\(, \\), \\1, [^"\\\\]). The
39
+ // grep pattern is interpolated from INSTALLER_ALLOW_ERE (its value has no backslashes).
40
+ function renderShim() {
41
+ return `#!/bin/sh
42
+ # Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; the installer AND the running
43
+ # guards binary both overwrite this file (self-healing) from renderShim(). Checked in on purpose so the
44
+ # hook has a stable, committed entry point even when node_modules is absent. Safe to delete along with
45
+ # the matching .claude/settings.json entries if you remove @webpieces/ai-hook-rules.
46
+ #
47
+ # Usage (wired into .claude/settings.json): sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" <bin-name>
48
+ BIN_NAME="$1"
49
+ shift
50
+ # Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the
51
+ # caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).
52
+ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
53
+ BIN="$ROOT/node_modules/.bin/$BIN_NAME"
54
+ if [ -x "$BIN" ]; then
55
+ exec "$BIN" "$@" # exec preserves stdin — hooks receive the tool payload as JSON on stdin
56
+ fi
57
+ # Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run.
58
+ # Before failing closed, peek at the tool payload and let ONLY package-manager install commands
59
+ # through: the assistant's own Bash tool routes through this hook too, so blocking everything would
60
+ # deadlock the one command (pnpm/npm install) that re-enables the guards. A silent exit 0 = "allow"
61
+ # in the PreToolUse protocol; the guards resume automatically once node_modules is present.
62
+ PAYLOAD="$(cat)"
63
+ CMD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
64
+ if printf '%s' "$CMD" | grep -Eq '${exports.INSTALLER_ALLOW_ERE}'; then
65
+ exit 0 # allow the installer so the assistant can self-heal the deadlock
66
+ fi
67
+ # Not an installer command → FAIL CLOSED. Deny via Claude Code's PreToolUse JSON protocol
68
+ # (permissionDecision "deny" on stdout, then exit 0) rather than a bare "exit 2". BOTH block the call,
69
+ # but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the
70
+ # model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never
71
+ # see the "run pnpm install" fix. This still fails closed: "deny" blocks the tool; it is not the silent
72
+ # allow a plain exit 0 with no JSON would be. The reason is a single JSON string with no
73
+ # double-quotes/backslashes, so it stays valid JSON after \${BIN_NAME} is substituted in.
74
+ REASON="❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (\${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)"
75
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\\n' "\$REASON"
76
+ exit 0 # decision is carried by permissionDecision "deny", not the exit code
77
+ `;
78
+ }
79
+ // Find the repo root that owns the committed shim to heal: walk up from `cwd` (the invocation's
80
+ // actual dir) to the nearest ancestor holding a shim, falling back to $CLAUDE_PROJECT_DIR (which
81
+ // Claude Code exports to hooks) only if the walk finds nothing. cwd-first keeps this correct for a
82
+ // nested clone and testable (a temp root is honoured over the ambient project env). Returns null when
83
+ // no committed shim exists (e.g. a global / absolute install, which has none to heal).
84
+ function findShimRoot(cwd) {
85
+ let dir = cwd;
86
+ for (;;) {
87
+ if (fs.existsSync(shimPath(dir)))
88
+ return dir;
89
+ const parent = path.dirname(dir);
90
+ if (parent === dir)
91
+ break;
92
+ dir = parent;
93
+ }
94
+ const env = process.env['CLAUDE_PROJECT_DIR'];
95
+ if (env && fs.existsSync(shimPath(env)))
96
+ return env;
97
+ return null;
98
+ }
99
+ // Best-effort: keep the committed shim identical to renderShim() so the fail-closed escape hatch and
100
+ // allowlist never drift. Only rewrites an EXISTING shim (never creates one) so global installs are
101
+ // untouched. NEVER throws — a self-heal must never block or crash a tool call.
102
+ function healShim(cwd) {
103
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
104
+ try {
105
+ const root = findShimRoot(cwd);
106
+ if (!root)
107
+ return;
108
+ const target = shimPath(root);
109
+ const desired = renderShim();
110
+ if (fs.readFileSync(target, 'utf8') === desired)
111
+ return;
112
+ fs.writeFileSync(target, desired, { mode: 0o755 });
113
+ fs.chmodSync(target, 0o755);
114
+ }
115
+ catch (err) {
116
+ //const error = toError(err);
117
+ // Ignore: healing is a convenience, not part of the guard decision.
118
+ }
119
+ }
120
+ //# sourceMappingURL=shim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shim.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim.ts"],"names":[],"mappings":";;;AAgBA,4BAEC;AAoBD,gCAsCC;AAuBD,4BAcC;;AAjHD,+CAAyB;AACzB,mDAA6B;AAE7B,8EAA8E;AAC9E,qGAAqG;AACrG,oGAAoG;AACpG,mGAAmG;AACnG,mGAAmG;AACnG,8BAA8B;AAC9B,EAAE;AACF,6FAA6F;AAC7F,qGAAqG;AACrG,oFAAoF;AACpF,8EAA8E;AACjE,QAAA,WAAW,GAAG,8BAA8B,CAAC;AAE1D,SAAgB,QAAQ,CAAC,WAAmB;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;AACxE,CAAC;AAED,6FAA6F;AAC7F,qGAAqG;AACrG,uGAAuG;AACvG,EAAE;AACF,iGAAiG;AACjG,mGAAmG;AACnG,6FAA6F;AAChF,QAAA,mBAAmB,GAC5B,qEAAqE,CAAC;AAE1E,iGAAiG;AACjG,qGAAqG;AACrG,oGAAoG;AACvF,QAAA,kBAAkB,GAAG,mDAAmD,CAAC;AAEtF,oGAAoG;AACpG,kGAAkG;AAClG,wFAAwF;AACxF,SAAgB,UAAU;IACtB,OAAO;;;;;;;;;;;;;;;;;;;;;;;oCAuByB,2BAAmB;;;;;;;;;;;;;CAatD,CAAC;AACF,CAAC;AAED,gGAAgG;AAChG,iGAAiG;AACjG,mGAAmG;AACnG,sGAAsG;AACtG,uFAAuF;AACvF,SAAS,YAAY,CAAC,GAAW;IAC7B,IAAI,GAAG,GAAG,GAAG,CAAC;IACd,SAAS,CAAC;QACN,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;QAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;QAC1B,GAAG,GAAG,MAAM,CAAC;IACjB,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAC9C,IAAI,GAAG,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC;IACpD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,qGAAqG;AACrG,mGAAmG;AACnG,+EAA+E;AAC/E,SAAgB,QAAQ,CAAC,GAAW;IAChC,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;QAC7B,IAAI,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,OAAO;YAAE,OAAO;QACxD,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACnD,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,oEAAoE;IACxE,CAAC;AACL,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\n// ---------------------------------------------------------------------------\n// The single checked-in shim (.claude/webpieces/ai-hook.sh). Both project hooks point at it, passing\n// their bin name as the first arg. settings.json points here (not at the bare bin) so a missing bin\n// (fresh clone, package removed) yields a friendly message instead of the raw `sh: No such file or\n// directory` on every Write/Edit/Bash tool call. `.claude` is committed, so the shim survives even\n// when node_modules does not.\n//\n// This module is the SINGLE SOURCE OF TRUTH for the shim body + the installer allowlist. The\n// installer (setup.ts) renders it on install; the running guards binary re-renders and self-heals it\n// (healShim) so the committed .sh can never go stale — no human ever hand-edits it.\n// ---------------------------------------------------------------------------\nexport const SHIM_MARKER = '.claude/webpieces/ai-hook.sh';\n\nexport function shimPath(projectRoot: string): string {\n return path.join(projectRoot, '.claude', 'webpieces', 'ai-hook.sh');\n}\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// Base command + `--flags` only. Because nothing but `--word` tokens may follow `install`, shell\n// operators (`;`, `&&`, `|`, backticks, `$()`, `>`, `<`) cannot match — so nothing can be smuggled\n// alongside the install. Keep in sync with INSTALLER_ALLOW_JS below (locked by a unit test).\nexport const INSTALLER_ALLOW_ERE =\n '^(pnpm|npm) install([[:space:]]+--[A-Za-z][A-Za-z-]*)*[[:space:]]*$';\n\n// JS-regex twin of INSTALLER_ALLOW_ERE (POSIX `[[:space:]]` → `\\s`). Not used by the fail-closed\n// shim (which is pure sh), but kept as the single JS-side definition should a future guard ever need\n// to recognise installer commands in the runner. A unit test asserts the two agree on a sample set.\nexport const INSTALLER_ALLOW_JS = /^(pnpm|npm) install(\\s+--[A-Za-z][A-Za-z-]*)*\\s*$/;\n\n// Normal template literal (not String.raw): it carries #235's shell escapes verbatim (\\${BIN_NAME},\n// \\$REASON, \\\\n for the deny JSON) AND my sed backslashes (doubled: \\\\(, \\\\), \\\\1, [^\"\\\\\\\\]). The\n// grep pattern is interpolated from INSTALLER_ALLOW_ERE (its value has no backslashes).\nexport function renderShim(): string {\n return `#!/bin/sh\n# Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; the installer AND the running\n# guards binary both overwrite this file (self-healing) from renderShim(). Checked in on purpose so the\n# hook has a stable, committed entry point even when node_modules is absent. Safe to delete along with\n# the matching .claude/settings.json entries if you remove @webpieces/ai-hook-rules.\n#\n# Usage (wired into .claude/settings.json): sh \"$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh\" <bin-name>\nBIN_NAME=\"$1\"\nshift\n# Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the\n# caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).\nROOT=\"$(CDPATH= cd -- \"$(dirname -- \"$0\")/../..\" && pwd)\"\nBIN=\"$ROOT/node_modules/.bin/$BIN_NAME\"\nif [ -x \"$BIN\" ]; then\n exec \"$BIN\" \"$@\" # exec preserves stdin — hooks receive the tool payload as JSON on stdin\nfi\n# Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run.\n# Before failing closed, peek at the tool payload and let ONLY package-manager install commands\n# through: the assistant's own Bash tool routes through this hook too, so blocking everything would\n# deadlock the one command (pnpm/npm install) that re-enables the guards. A silent exit 0 = \"allow\"\n# in the PreToolUse protocol; the guards resume automatically once node_modules is present.\nPAYLOAD=\"$(cat)\"\nCMD=\"$(printf '%s' \"$PAYLOAD\" | sed -n 's/.*\"command\"[[:space:]]*:[[:space:]]*\"\\\\([^\"\\\\\\\\]*\\\\)\".*/\\\\1/p')\"\nif printf '%s' \"$CMD\" | grep -Eq '${INSTALLER_ALLOW_ERE}'; then\n exit 0 # allow the installer so the assistant can self-heal the deadlock\nfi\n# Not an installer command → FAIL CLOSED. Deny via Claude Code's PreToolUse JSON protocol\n# (permissionDecision \"deny\" on stdout, then exit 0) rather than a bare \"exit 2\". BOTH block the call,\n# but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the\n# model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never\n# see the \"run pnpm install\" fix. This still fails closed: \"deny\" blocks the tool; it is not the silent\n# allow a plain exit 0 with no JSON would be. The reason is a single JSON string with no\n# double-quotes/backslashes, so it stays valid JSON after \\${BIN_NAME} is substituted in.\nREASON=\"❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (\\${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)\"\nprintf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"%s\"}}\\\\n' \"\\$REASON\"\nexit 0 # decision is carried by permissionDecision \"deny\", not the exit code\n`;\n}\n\n// Find the repo root that owns the committed shim to heal: walk up from `cwd` (the invocation's\n// actual dir) to the nearest ancestor holding a shim, falling back to $CLAUDE_PROJECT_DIR (which\n// Claude Code exports to hooks) only if the walk finds nothing. cwd-first keeps this correct for a\n// nested clone and testable (a temp root is honoured over the ambient project env). Returns null when\n// no committed shim exists (e.g. a global / absolute install, which has none to heal).\nfunction findShimRoot(cwd: string): string | null {\n let dir = cwd;\n for (;;) {\n if (fs.existsSync(shimPath(dir))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n const env = process.env['CLAUDE_PROJECT_DIR'];\n if (env && fs.existsSync(shimPath(env))) return env;\n return null;\n}\n\n// Best-effort: keep the committed shim identical to renderShim() so the fail-closed escape hatch and\n// allowlist never drift. Only rewrites an EXISTING shim (never creates one) so global installs are\n// untouched. NEVER throws — a self-heal must never block or crash a tool call.\nexport function healShim(cwd: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const root = findShimRoot(cwd);\n if (!root) return;\n const target = shimPath(root);\n const desired = renderShim();\n if (fs.readFileSync(target, 'utf8') === desired) return;\n fs.writeFileSync(target, desired, { mode: 0o755 });\n fs.chmodSync(target, 0o755);\n } catch (err: unknown) {\n //const error = toError(err);\n // Ignore: healing is a convenience, not part of the guard decision.\n }\n}\n"]}
@@ -0,0 +1,36 @@
1
+ #!/bin/sh
2
+ # Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; the installer AND the running
3
+ # guards binary both overwrite this file (self-healing) from renderShim(). Checked in on purpose so the
4
+ # hook has a stable, committed entry point even when node_modules is absent. Safe to delete along with
5
+ # the matching .claude/settings.json entries if you remove @webpieces/ai-hook-rules.
6
+ #
7
+ # Usage (wired into .claude/settings.json): sh "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" <bin-name>
8
+ BIN_NAME="$1"
9
+ shift
10
+ # Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the
11
+ # caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).
12
+ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
13
+ BIN="$ROOT/node_modules/.bin/$BIN_NAME"
14
+ if [ -x "$BIN" ]; then
15
+ exec "$BIN" "$@" # exec preserves stdin — hooks receive the tool payload as JSON on stdin
16
+ fi
17
+ # Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run.
18
+ # Before failing closed, peek at the tool payload and let ONLY package-manager install commands
19
+ # through: the assistant's own Bash tool routes through this hook too, so blocking everything would
20
+ # deadlock the one command (pnpm/npm install) that re-enables the guards. A silent exit 0 = "allow"
21
+ # in the PreToolUse protocol; the guards resume automatically once node_modules is present.
22
+ PAYLOAD="$(cat)"
23
+ CMD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
24
+ if printf '%s' "$CMD" | grep -Eq '^(pnpm|npm) install([[:space:]]+--[A-Za-z][A-Za-z-]*)*[[:space:]]*$'; then
25
+ exit 0 # allow the installer so the assistant can self-heal the deadlock
26
+ fi
27
+ # Not an installer command → FAIL CLOSED. Deny via Claude Code's PreToolUse JSON protocol
28
+ # (permissionDecision "deny" on stdout, then exit 0) rather than a bare "exit 2". BOTH block the call,
29
+ # but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the
30
+ # model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never
31
+ # see the "run pnpm install" fix. This still fails closed: "deny" blocks the tool; it is not the silent
32
+ # allow a plain exit 0 with no JSON would be. The reason is a single JSON string with no
33
+ # double-quotes/backslashes, so it stays valid JSON after ${BIN_NAME} is substituted in.
34
+ REASON="❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)"
35
+ printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$REASON"
36
+ exit 0 # decision is carried by permissionDecision "deny", not the exit code
@@ -1,32 +0,0 @@
1
- #!/bin/sh
2
- # Managed by @webpieces/ai-hook-rules (wp-setup-ai-hooks) — do not edit; re-running the installer
3
- # overwrites this file. Checked in on purpose so the hook has a stable, committed entry point even
4
- # when node_modules is absent. Safe to delete along with the matching .claude/settings.json entries
5
- # if you remove @webpieces/ai-hook-rules.
6
- #
7
- # Usage (wired into .claude/settings.json): "$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh" <bin-name>
8
- #
9
- # This template mirrors renderShim() in src/bin/setup.ts, which is the source of truth the installer
10
- # actually writes into a consumer repo. It lives here for reference / code review; keep it in sync.
11
- BIN_NAME="$1"
12
- shift
13
- # Resolve the bin relative to THIS script (…/<root>/.claude/webpieces/ai-hook.sh → <root>), not the
14
- # caller's cwd — the hook can be invoked from any directory (a subdir, or a nested clone).
15
- ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
16
- BIN="$ROOT/node_modules/.bin/$BIN_NAME"
17
- if [ -x "$BIN" ]; then
18
- exec "$BIN" "$@" # exec preserves stdin — hooks receive the tool payload as JSON on stdin
19
- fi
20
- # Bin missing (fresh clone before install, or a broken install). The webpieces guards CANNOT run, so
21
- # FAIL CLOSED — deny the tool call. We deny via Claude Code's PreToolUse JSON protocol
22
- # (permissionDecision "deny" on stdout, then exit 0) rather than a bare "exit 2". BOTH block the call,
23
- # but only the JSON's permissionDecisionReason is surfaced to the human in the terminal UI (and to the
24
- # model) — an exit-2 stderr message is NOT reliably shown on a blocked call, so the user would never
25
- # see the "run pnpm install" fix (they'd just see the tool's optimistic action summary). This still
26
- # fails closed: permissionDecision "deny" blocks the tool; it is not silently allowed like a plain
27
- # exit 0 would be. Tell the human to install; their own terminal 'pnpm install' does not go through
28
- # this hook. The reason is a single JSON string with no double-quotes/backslashes, so it stays valid
29
- # JSON after ${BIN_NAME} (always wp-ai-rules-hook / wp-ai-guards-hook) is substituted in.
30
- REASON="❌ @webpieces/ai-hook-rules is declared in package.json but is not installed (${BIN_NAME} not found). Run 'pnpm install' (or this repo's installer) to enable the webpieces AI guards, then retry. (If you removed @webpieces/ai-hook-rules on purpose, delete its hooks from .claude/settings.json.)"
31
- printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$REASON"
32
- exit 0 # decision is carried by permissionDecision "deny", not the exit code