@codyswann/lisa 4.41.1 → 4.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +3 -3
  2. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +327 -0
  3. package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.sh +57 -0
  4. package/all/copy-overwrite/scripts/lisa-work-item.mjs +49 -3
  5. package/all/merge/.claude/settings.json +11 -0
  6. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  7. package/dist/core/lisa-owned-hash-ledger.js +22 -0
  8. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  9. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  10. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  11. package/dist/core/upstream-evidence-manifest.js +21 -6
  12. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  13. package/package.json +4 -4
  14. package/plugins/lisa/.claude-plugin/plugin.json +18 -1
  15. package/plugins/lisa/.codex-plugin/hooks.json +17 -0
  16. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa/hooks/worktree-binding-guard.mjs +324 -0
  18. package/plugins/lisa/hooks/worktree-binding-guard.sh +54 -0
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +18 -1
  26. package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +324 -0
  27. package/plugins/lisa-copilot/hooks/worktree-binding-guard.sh +54 -0
  28. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cursor/hooks/hooks.json +12 -0
  30. package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +324 -0
  31. package/plugins/lisa-cursor/hooks/worktree-binding-guard.sh +54 -0
  32. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo-agy/plugin.json +1 -1
  35. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  55. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails-agy/plugin.json +1 -1
  60. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  65. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  70. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/materialized-artifacts.json +2 -0
  73. package/plugins/src/base/.claude-plugin/plugin.json +17 -0
  74. package/plugins/src/base/hooks/worktree-binding-guard.mjs +324 -0
  75. package/plugins/src/base/hooks/worktree-binding-guard.sh +54 -0
  76. package/scripts/build-plugins.sh +2 -2
  77. package/scripts/lisa-enforcement-fallback.sh +3 -3
  78. package/scripts/two-channel-couplings.json +1 -1
package/package.json CHANGED
@@ -171,7 +171,7 @@
171
171
  "zod-validation-error": "^4.0.0"
172
172
  },
173
173
  "name": "@codyswann/lisa",
174
- "version": "4.41.1",
174
+ "version": "4.42.1",
175
175
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
176
176
  "main": "dist/index.js",
177
177
  "exports": {
@@ -319,7 +319,7 @@
319
319
  "test": "tests"
320
320
  },
321
321
  "types": "./dist/index.d.ts",
322
- "lisaReleaseCommit": "f53413636a353ff14e517508836882aa5f36429a",
323
- "gitHead": "f53413636a353ff14e517508836882aa5f36429a",
324
- "lisaReleaseTag": "v4.41.1"
322
+ "lisaReleaseCommit": "76a0d0db994047aee8067e20e77620f84fc6faba",
323
+ "gitHead": "76a0d0db994047aee8067e20e77620f84fc6faba",
324
+ "lisaReleaseTag": "v4.42.1"
325
325
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.41.1",
3
+ "version": "4.42.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -107,6 +107,15 @@
107
107
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/withdrawn-rulings.sh --hook"
108
108
  }
109
109
  ]
110
+ },
111
+ {
112
+ "matcher": "EnterWorktree",
113
+ "hooks": [
114
+ {
115
+ "type": "command",
116
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
117
+ }
118
+ ]
110
119
  }
111
120
  ],
112
121
  "PreToolUse": [
@@ -149,6 +158,10 @@
149
158
  {
150
159
  "type": "command",
151
160
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-managed-file-edits.sh"
161
+ },
162
+ {
163
+ "type": "command",
164
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
152
165
  }
153
166
  ]
154
167
  },
@@ -162,6 +175,10 @@
162
175
  {
163
176
  "type": "command",
164
177
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-managed-file-edits.sh"
178
+ },
179
+ {
180
+ "type": "command",
181
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
165
182
  }
166
183
  ]
167
184
  },
@@ -56,6 +56,15 @@
56
56
  "command": "${PLUGIN_ROOT}/hooks/withdrawn-rulings.sh --hook"
57
57
  }
58
58
  ]
59
+ },
60
+ {
61
+ "matcher": "EnterWorktree",
62
+ "hooks": [
63
+ {
64
+ "type": "command",
65
+ "command": "${PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
66
+ }
67
+ ]
59
68
  }
60
69
  ],
61
70
  "PreToolUse": [
@@ -89,6 +98,10 @@
89
98
  {
90
99
  "type": "command",
91
100
  "command": "${PLUGIN_ROOT}/hooks/block-managed-file-edits.sh"
101
+ },
102
+ {
103
+ "type": "command",
104
+ "command": "${PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
92
105
  }
93
106
  ]
94
107
  },
@@ -102,6 +115,10 @@
102
115
  {
103
116
  "type": "command",
104
117
  "command": "${PLUGIN_ROOT}/hooks/block-managed-file-edits.sh"
118
+ },
119
+ {
120
+ "type": "command",
121
+ "command": "${PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
105
122
  }
106
123
  ]
107
124
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.41.1",
3
+ "version": "4.42.1",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reconcile the worktree a session was TOLD it is in against the one it is
4
+ * ACTUALLY operating in, and refuse to act while the two disagree.
5
+ *
6
+ * WHAT WENT WRONG. `EnterWorktree` reported "the session is now working in the
7
+ * worktree" for a tree that Bash never moved to and that Edit then refused by
8
+ * name (CodySwannGT/lisa#3864). Three subsystems, three answers, and the one
9
+ * that answered affirmatively was the wrong one. A refusal teaches you
10
+ * something; a success message that is not true is a confirmation — it removes
11
+ * the reason to check and sends the agent forward confidently in the wrong
12
+ * place. Acting on that confirmation, one session ran an install that landed in
13
+ * a different agent's active worktree. The next such command is a `git commit`
14
+ * or a file write, and that tree held eighteen modified tracked files.
15
+ *
16
+ * The displacement is also mutual: whoever calls `EnterWorktree` last wins for
17
+ * every session whose own binding never took, so each agent repairing itself
18
+ * displaces the others. That is why "just re-enter your worktree" is not a
19
+ * remedy — it is the mechanism.
20
+ *
21
+ * WHAT THIS FIXES AND WHAT IT CANNOT. Lisa does not own `EnterWorktree`, so it
22
+ * cannot make that call itself return failure. What it can do is make the
23
+ * false confirmation cost nothing: the FIRST action taken on the strength of it
24
+ * is refused, and the refusal names the worktree the session is really bound
25
+ * to. The gap is stated rather than hidden — the acceptance criterion asking
26
+ * `EnterWorktree` to fail is met one tool call later, by the guard, not by the
27
+ * tool.
28
+ *
29
+ * WHY BLOCKING AND NOT A WARNING. The failure mode is that nothing feels wrong.
30
+ * There is no symptom to notice, so an advisory line scrolls past exactly when
31
+ * it matters. A refusal is the only form of this check the agent cannot read
32
+ * past.
33
+ *
34
+ * WHY AN EXPLICIT ACKNOWLEDGEMENT AND NOT "BLOCK ONCE". A guard that blocks the
35
+ * first attempt and lets the retry through is defeated by a blind retry, which
36
+ * is the single most likely next action. So the block stands until the session
37
+ * states, in the acknowledgement, the absolute path it now intends to work in.
38
+ * A path that does not match what the session is actually in is refused too —
39
+ * that is the rejection control, and it is what stops a stale acknowledgement
40
+ * copied from another agent's transcript from silently rebinding this one.
41
+ *
42
+ * FAILING OPEN, LOUDLY. Anything this cannot determine — no session id, no
43
+ * cwd, not a git repository, an unreadable state file — exits 0 with a line on
44
+ * stderr. A guard that cannot read its input cannot tell a displacement from a
45
+ * directory listing, and one that wedges every tool call is switched off within
46
+ * the hour. Silence is the only outcome that is never acceptable.
47
+ */
48
+
49
+ import { execFileSync } from "node:child_process";
50
+ import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
51
+ import { homedir } from "node:os";
52
+ import { dirname, isAbsolute, join, resolve } from "node:path";
53
+
54
+ /** Tools whose effect on the wrong worktree is not undoable by re-reading. */
55
+ const GUARDED_TOOLS = new Set(["Bash", "Write", "Edit", "MultiEdit"]);
56
+
57
+ /**
58
+ * The literal an agent echoes to rebind this session deliberately.
59
+ *
60
+ * A shell line rather than a flag on this script, because the only surface the
61
+ * agent has for saying so is a Bash command, and it must be one this guard sees
62
+ * on its own PreToolUse pass — which is exactly where a command string arrives.
63
+ */
64
+ const ACCEPT_PREFIX = "lisa-worktree-binding: accept";
65
+
66
+ /**
67
+ * Resolve a path the way git reports one.
68
+ *
69
+ * `git rev-parse --show-toplevel` answers with symlinks resolved, and on macOS
70
+ * every temporary directory is a symlink — `/var/…` is `/private/var/…`. A
71
+ * comparison between git's answer and a path handed in by a tool would then
72
+ * differ by that prefix alone and read as a displacement, which is a false
73
+ * accusation in exactly the state the guard is supposed to be trusted in. A
74
+ * path that does not exist yet cannot be resolved and is returned as given.
75
+ * @param value - Absolute path to normalize
76
+ * @returns The path with symlinks resolved where possible
77
+ */
78
+ function realpath(value) {
79
+ try {
80
+ return realpathSync(value);
81
+ } catch {
82
+ return value;
83
+ }
84
+ }
85
+
86
+ function say(message) {
87
+ process.stderr.write(`worktree-binding-guard: ${message}\n`);
88
+ }
89
+
90
+ /** Run git, returning trimmed stdout, or null when it exits non-zero. */
91
+ function git(args, cwd) {
92
+ try {
93
+ return execFileSync("git", args, {
94
+ cwd,
95
+ encoding: "utf8",
96
+ stdio: ["ignore", "pipe", "ignore"],
97
+ }).trim();
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /** The worktree root containing `cwd`, or null when `cwd` is not in a repo. */
104
+ function worktreeRoot(cwd) {
105
+ const top = git(["rev-parse", "--show-toplevel"], cwd);
106
+ return top ? resolve(top) : null;
107
+ }
108
+
109
+ /**
110
+ * The MAIN checkout's root, which is where `.claude/worktrees/` lives.
111
+ *
112
+ * `--git-common-dir` is shared by every linked worktree, so its parent is the
113
+ * main checkout no matter which worktree asks.
114
+ */
115
+ function mainCheckout(cwd) {
116
+ const common = git(
117
+ ["rev-parse", "--path-format=absolute", "--git-common-dir"],
118
+ cwd
119
+ );
120
+ return common ? dirname(resolve(common)) : null;
121
+ }
122
+
123
+ function stateFile(sessionId) {
124
+ const home = process.env.LISA_STATE_HOME || join(homedir(), ".lisa");
125
+ return join(home, "worktree-binding", `${sessionId}.json`);
126
+ }
127
+
128
+ function readState(sessionId) {
129
+ try {
130
+ return JSON.parse(readFileSync(stateFile(sessionId), "utf8"));
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ function writeState(sessionId, state) {
137
+ const file = stateFile(sessionId);
138
+ try {
139
+ mkdirSync(dirname(file), { recursive: true });
140
+ writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`);
141
+ return true;
142
+ } catch (error) {
143
+ say(`could not record the binding (${error.message}); NOT enforcing`);
144
+ return false;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * The worktree `EnterWorktree` claims the session moved to.
150
+ *
151
+ * `path` is given verbatim; `name` resolves under the main checkout's
152
+ * `.claude/worktrees/`, which is where the tool creates one. A call carrying
153
+ * neither is a generated name this guard cannot predict, so it returns null and
154
+ * the claim is simply not recorded — an unrecorded claim degrades to the plain
155
+ * displacement check rather than to a false accusation.
156
+ */
157
+ function claimedRoot(toolInput, cwd) {
158
+ const path = toolInput?.path;
159
+ if (typeof path === "string" && path.trim()) {
160
+ return realpath(resolve(isAbsolute(path) ? path : join(cwd, path)));
161
+ }
162
+ const name = toolInput?.name;
163
+ if (typeof name === "string" && name.trim()) {
164
+ const main = mainCheckout(cwd);
165
+ return main
166
+ ? realpath(join(main, ".claude", "worktrees", name.trim()))
167
+ : null;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * Record what `EnterWorktree` just claimed, for the next tool call to check.
174
+ *
175
+ * Returns nothing on purpose. This runs on PostToolUse, where the tool has
176
+ * already happened and there is no call left to refuse, so every path through
177
+ * it is an allow — and a function whose only answer is "allow" should not be
178
+ * spelled as one that computes an exit code.
179
+ */
180
+ function recordClaim(payload, observed) {
181
+ const claimed = claimedRoot(payload.tool_input, payload.cwd);
182
+ if (!claimed || claimed === observed) return;
183
+ const previous = readState(payload.session_id);
184
+ writeState(payload.session_id, {
185
+ boundRoot: previous?.boundRoot ?? observed,
186
+ claimedRoot: claimed,
187
+ updatedAt: new Date().toISOString(),
188
+ });
189
+ }
190
+
191
+ function refuse(lines) {
192
+ process.stderr.write(`${lines.join("\n")}\n`);
193
+ return 2;
194
+ }
195
+
196
+ function acceptanceLine(observed) {
197
+ return ` echo '${ACCEPT_PREFIX} ${observed}'`;
198
+ }
199
+
200
+ /**
201
+ * Handle an acknowledgement line, or return null when the command is not one.
202
+ *
203
+ * The path is compared against what the session is measurably in, not against
204
+ * what it says it wants. An acknowledgement naming somewhere else is the case
205
+ * this is for: it means the operator is reasoning about a tree they are not in.
206
+ */
207
+ function handleAcceptance(payload, observed) {
208
+ const command = payload.tool_input?.command;
209
+ if (typeof command !== "string" || !command.includes(ACCEPT_PREFIX))
210
+ return null;
211
+ const stated = command.slice(
212
+ command.indexOf(ACCEPT_PREFIX) + ACCEPT_PREFIX.length
213
+ );
214
+ const match = /([^\s'"]+)/.exec(stated);
215
+ const path = match ? realpath(resolve(match[1])) : null;
216
+ if (path !== observed) {
217
+ return refuse([
218
+ `worktree-binding-guard: this acknowledgement names ${path ?? "no path"},`,
219
+ `but this session is operating in ${observed}. Not rebinding.`,
220
+ `If ${observed} is where you mean to work, acknowledge that path:`,
221
+ acceptanceLine(observed),
222
+ ]);
223
+ }
224
+ writeState(payload.session_id, {
225
+ boundRoot: observed,
226
+ claimedRoot: null,
227
+ updatedAt: new Date().toISOString(),
228
+ });
229
+ say(`bound to ${observed}`);
230
+ return 0;
231
+ }
232
+
233
+ /** Refusal text for a switch that reported success without taking effect. */
234
+ function unconfirmedSwitch(claimed, observed) {
235
+ return refuse([
236
+ `worktree-binding-guard: EnterWorktree reported success for`,
237
+ ` ${claimed}`,
238
+ `but this session is still operating in`,
239
+ ` ${observed}`,
240
+ "The switch did not take effect. Anything run now lands in the second",
241
+ "path, not the first — including installs, commits, and file writes, and",
242
+ "that tree may hold another agent's uncommitted work.",
243
+ "",
244
+ "Use absolute paths under the tree you actually want, or acknowledge that",
245
+ "you intend to keep working where you are:",
246
+ acceptanceLine(observed),
247
+ ]);
248
+ }
249
+
250
+ /** Refusal text for a binding that moved without this session asking. */
251
+ function displaced(bound, observed) {
252
+ return refuse([
253
+ "worktree-binding-guard: this session's worktree changed underneath it.",
254
+ ` bound to: ${bound}`,
255
+ ` now in: ${observed}`,
256
+ "Concurrent sessions share one worktree binding, so another agent's",
257
+ "EnterWorktree can move this one. Commands run now land in the second",
258
+ "path. If you moved deliberately, say so:",
259
+ acceptanceLine(observed),
260
+ ]);
261
+ }
262
+
263
+ function evaluate(payload) {
264
+ const sessionId = payload.session_id;
265
+ const cwd = payload.cwd;
266
+ if (typeof sessionId !== "string" || !sessionId || typeof cwd !== "string") {
267
+ say("payload carries no session id or cwd; binding is NOT enforced");
268
+ return 0;
269
+ }
270
+ const observed = worktreeRoot(cwd);
271
+ if (!observed) return 0;
272
+
273
+ if (payload.tool_name === "EnterWorktree") {
274
+ recordClaim(payload, observed);
275
+ return 0;
276
+ }
277
+ if (!GUARDED_TOOLS.has(payload.tool_name)) return 0;
278
+
279
+ const accepted = handleAcceptance(payload, observed);
280
+ if (accepted !== null) return accepted;
281
+
282
+ const state = readState(sessionId);
283
+ if (!state?.boundRoot) {
284
+ writeState(sessionId, {
285
+ boundRoot: observed,
286
+ claimedRoot: null,
287
+ updatedAt: new Date().toISOString(),
288
+ });
289
+ return 0;
290
+ }
291
+ if (state.claimedRoot && state.claimedRoot !== observed) {
292
+ return unconfirmedSwitch(state.claimedRoot, observed);
293
+ }
294
+ if (state.claimedRoot === observed) {
295
+ writeState(sessionId, {
296
+ boundRoot: observed,
297
+ claimedRoot: null,
298
+ updatedAt: new Date().toISOString(),
299
+ });
300
+ return 0;
301
+ }
302
+ if (state.boundRoot !== observed) return displaced(state.boundRoot, observed);
303
+ return 0;
304
+ }
305
+
306
+ function main() {
307
+ let raw = "";
308
+ try {
309
+ raw = readFileSync(0, "utf8");
310
+ } catch {
311
+ say("could not read the hook payload; binding is NOT enforced");
312
+ return 0;
313
+ }
314
+ let payload;
315
+ try {
316
+ payload = JSON.parse(raw);
317
+ } catch {
318
+ say("hook payload was not JSON; binding is NOT enforced");
319
+ return 0;
320
+ }
321
+ return evaluate(payload);
322
+ }
323
+
324
+ process.exit(main());
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse guard for Bash/Write/Edit/MultiEdit, and PostToolUse recorder for
3
+ # EnterWorktree: refuse to act while the worktree the session was told it is in
4
+ # disagrees with the one it is measurably in (CodySwannGT/lisa#3864).
5
+ #
6
+ # The reconciliation lives in worktree-binding-guard.mjs beside this file. It
7
+ # needs per-session state, path resolution and git plumbing, and a bash
8
+ # reimplementation of any of that would be a second copy of the thing the guard
9
+ # exists to keep honest.
10
+ #
11
+ # This wrapper does three things and nothing else: probe its interpreter, pass
12
+ # the hook envelope through untouched, and return the classifier's exit code so
13
+ # a refusal is a refusal.
14
+ #
15
+ # FAILING OPEN, LOUDLY
16
+ #
17
+ # Claude Code treats exit 2 as a refusal and every OTHER non-zero exit as a
18
+ # non-blocking hook error, so a missing interpreter under `set -e` would exit
19
+ # 127 and permit exactly what this exists to stop. Degrading to "allow" is
20
+ # correct — a hook that cannot read its input cannot tell a displacement from a
21
+ # directory listing — but it must SAY so, because a guard that is silently
22
+ # absent reads exactly like a guard that is passing. Same reasoning as
23
+ # block-host-name-leak.sh.
24
+ #
25
+ # AGENT PARITY, AND WHY IT STOPS WHERE IT DOES
26
+ #
27
+ # `EnterWorktree` is a Claude Code tool, and the process-global session binding
28
+ # it rewrites is a Claude Code structure. Codex, Antigravity and OpenCode manage
29
+ # their own worktrees and have neither, so there is nothing on those harnesses
30
+ # for the reconciler to reconcile — no `.agy.sh` adapter is shipped for this
31
+ # guard, and its absence is a statement rather than an omission. The Codex
32
+ # variant carries the PreToolUse half because the displacement check is
33
+ # harness-agnostic: it only asks whether the directory a session is measurably
34
+ # in has changed since it last acted.
35
+ set -uo pipefail
36
+
37
+ input="$(cat)"
38
+
39
+ if ! command -v node >/dev/null 2>&1; then
40
+ printf 'worktree-binding-guard: node not found; worktree-binding enforcement is NOT active\n' >&2
41
+ exit 0
42
+ fi
43
+
44
+ hook_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
45
+ classifier="$hook_dir/worktree-binding-guard.mjs"
46
+ if [ ! -r "$classifier" ]; then
47
+ printf 'worktree-binding-guard: classifier missing at %s; worktree-binding enforcement is NOT active\n' \
48
+ "$classifier" >&2
49
+ exit 0
50
+ fi
51
+
52
+ status=0
53
+ printf '%s' "$input" | node "$classifier" || status=$?
54
+ exit "$status"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.41.1",
3
+ "version": "4.42.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.41.1",
3
+ "version": "4.42.1",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.41.1",
3
+ "version": "4.42.1",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.41.1",
3
+ "version": "4.42.1",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.41.1",
3
+ "version": "4.42.1",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.41.1",
3
+ "version": "4.42.1",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.41.1",
3
+ "version": "4.42.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -62,6 +62,15 @@
62
62
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/withdrawn-rulings.sh --hook"
63
63
  }
64
64
  ]
65
+ },
66
+ {
67
+ "matcher": "EnterWorktree",
68
+ "hooks": [
69
+ {
70
+ "type": "command",
71
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
72
+ }
73
+ ]
65
74
  }
66
75
  ],
67
76
  "preToolUse": [
@@ -95,6 +104,10 @@
95
104
  {
96
105
  "type": "command",
97
106
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-managed-file-edits.sh"
107
+ },
108
+ {
109
+ "type": "command",
110
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
98
111
  }
99
112
  ]
100
113
  },
@@ -108,6 +121,10 @@
108
121
  {
109
122
  "type": "command",
110
123
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-managed-file-edits.sh"
124
+ },
125
+ {
126
+ "type": "command",
127
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
111
128
  }
112
129
  ]
113
130
  },