agent-sanitizer 2.47.2 → 2.47.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -250,6 +250,8 @@ To wire them yourself instead, one entry dispatches every mode on `--hook=`:
250
250
 
251
251
  **Wire all five.** The instruction-file scan is split across two of them: `SessionStart` covers the files that load at launch, and `InstructionsLoaded` covers every one a subdirectory loads later. A host that wires the first without the second leaves a nested `CLAUDE.md` scanned by nothing, and the one-time PreToolUse coverage notice is the only thing that says so.
252
252
 
253
+ `InstructionsLoaded` needs Claude Code **2.1.69 or newer** — 2.1.68 does not emit the event, so the hook never fires however you wire it. Check with `claude --version`.
254
+
253
255
  `require.resolve("agent-sanitizer/claude-hooks")` gives the path without
254
256
  hardcoding a layout. Importing the module rather than spawning it is a no-op.
255
257
 
package/THREAT-MODEL.md CHANGED
@@ -377,8 +377,9 @@ confined to `CLAUDE_PROJECT_DIR` in both — an ancestor file, or one under
377
377
  `~/.claude`, is shared with every other project on the machine, so it is reported
378
378
  through the cross-hook alert and never rewritten. Three things lose the lazy half
379
379
  entirely: a host that never wired the `InstructionsLoaded` event to
380
- `scan-loaded-instructions`, a Claude Code build that does not emit that event,
381
- and `scan-loaded-instructions` switched off in `AGENT_SANITIZER_DISABLED_HOOKS`.
380
+ `scan-loaded-instructions`, a Claude Code older than 2.1.69, which is the first
381
+ build that emits that event, and `scan-loaded-instructions` switched off in
382
+ `AGENT_SANITIZER_DISABLED_HOOKS`.
382
383
  Nothing on disk tells them apart, so the PreToolUse gate names all three, once
383
384
  per session, rather than leaving the gap silent.
384
385
 
@@ -302,6 +302,14 @@ export function recordInstructionsLoaded(sessionId) {
302
302
  sweepStaleSessions(sessionId);
303
303
  }
304
304
 
305
+ /**
306
+ * The first `@anthropic-ai/claude-code` release that emits `InstructionsLoaded`.
307
+ * 2.1.68 carries no occurrence of the event name and 2.1.69 carries eight, so a
308
+ * CLI below this floor never fires the hook however it is wired. The gap notice
309
+ * quotes it: "upgrade" is unactionable without the number to compare against.
310
+ */
311
+ const EVENT_MIN_CLI_VERSION = "2.1.69";
312
+
305
313
  /**
306
314
  * The one-time context line for a session where no InstructionsLoaded scan ran,
307
315
  * or null when the scan has been seen or the notice was already surfaced this
@@ -318,10 +326,10 @@ export function recordInstructionsLoaded(sessionId) {
318
326
  *
319
327
  * The notice names the OBSERVABLE — no scan ran — and all three causes, because
320
328
  * the marker cannot tell them apart: a host that never wired the event to
321
- * scan-loaded-instructions, a Claude Code that does not emit it, and the hook
322
- * switched off in AGENT_SANITIZER_DISABLED_HOOKS; asserting one sends a reader
323
- * who is in another to the wrong fix. The wiring cause leads because it is the
324
- * only one the reader can repair in this session, and nothing else reports it.
329
+ * scan-loaded-instructions, a Claude Code older than EVENT_MIN_CLI_VERSION, and
330
+ * the hook switched off in AGENT_SANITIZER_DISABLED_HOOKS; asserting one sends a
331
+ * reader who is in another to the wrong fix. The wiring cause leads because it is
332
+ * the only one the reader can repair in this session, and nothing else reports it.
325
333
  * @param {string} [sessionId] the harness's session identity, so the answer
326
334
  * belongs to THIS session (see instructionsLoadedFile)
327
335
  * @returns {string | null}
@@ -334,11 +342,15 @@ export function instructionsLoadedGapNotice(sessionId) {
334
342
  "instruction files loaded from SUBDIRECTORIES (a nested CLAUDE.md, a " +
335
343
  "directory-scoped rule) are reaching the model unscanned for hidden " +
336
344
  "Unicode — the session-start scan covers only the files loaded at launch. " +
337
- "Tell the user, and name all three causes: this host never wired the " +
338
- "InstructionsLoaded event to scan-loaded-instructions (wiring it restores " +
339
- "the coverage), a Claude Code that does not emit the event (upgrading " +
340
- "restores it), or scan-loaded-instructions switched off in " +
341
- "AGENT_SANITIZER_DISABLED_HOOKS."
345
+ "Tell the user, and name all three causes with the command that decides " +
346
+ "each: this host never wired the InstructionsLoaded event to " +
347
+ "scan-loaded-instructions (the `/hooks` command lists what this session " +
348
+ "actually registered, whichever config dir or plugin root the host uses; " +
349
+ "wiring it restores the coverage), a Claude Code " +
350
+ `older than ${EVENT_MIN_CLI_VERSION}, the first build that emits the event ` +
351
+ "(`claude --version`; upgrading restores it), or scan-loaded-instructions " +
352
+ "switched off in AGENT_SANITIZER_DISABLED_HOOKS (`echo " +
353
+ "$AGENT_SANITIZER_DISABLED_HOOKS`)."
342
354
  );
343
355
  }
344
356
 
@@ -81,7 +81,8 @@ const HOOK_NAME = "pretooluse-sanitize";
81
81
  * must be blocked, or null to let the pipeline continue. Hosts use these for
82
82
  * policy the package has no view of (a required workflow step, a project-local
83
83
  * rule); the package ships none.
84
- * @typedef {(input: { tool_name: string | null, tool_input: any, session_id?: string })
84
+ * @typedef {(input: { tool_name: string | null, tool_input: any, session_id?: string,
85
+ * permission_mode?: string })
85
86
  * => string | null | undefined} HostGate
86
87
  */
87
88
 
@@ -595,13 +596,16 @@ export async function judgePreToolUseSanitize(event, rehydrate, opts = {}) {
595
596
  // a pass is the one incentive a gate must never create.
596
597
  if (event.event === EventKind.UNKNOWN)
597
598
  return { decision: Decision.DENY, reason: messages.unknownEvent };
598
- // The session identity travels in `meta`, not alongside the tool input; a host
599
- // gate keyed on the session (a once-per-session checkpoint) cannot tell two
600
- // sessions apart without it.
599
+ // The session identity and the permission mode travel in `meta`, not alongside
600
+ // the tool input. A gate keyed on the session (a once-per-session checkpoint)
601
+ // cannot tell two sessions apart without the first, and a gate keyed on the
602
+ // mode reads `undefined` without the second — so it fires in EVERY mode, which
603
+ // is the safe direction but not the intended one.
601
604
  const input = {
602
605
  tool_name: event.tool,
603
606
  tool_input: event.input,
604
607
  session_id: event.meta?.session_id,
608
+ permission_mode: event.meta?.permission_mode,
605
609
  };
606
610
  // Host gates run BEFORE any rewriting layer, because they decide whether the
607
611
  // call may happen at all rather than what its input contains — and returning
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sanitizer",
3
- "version": "2.47.2",
3
+ "version": "2.47.4",
4
4
  "description": "Defend an agent against hidden-content injection: strip payload-capable invisible Unicode and ANSI, splice out human-invisible HTML, and flag data-exfil URLs in untrusted text before any model sees it.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -90,10 +90,10 @@ export function recordInstructionsLoaded(sessionId?: string): void;
90
90
  *
91
91
  * The notice names the OBSERVABLE — no scan ran — and all three causes, because
92
92
  * the marker cannot tell them apart: a host that never wired the event to
93
- * scan-loaded-instructions, a Claude Code that does not emit it, and the hook
94
- * switched off in AGENT_SANITIZER_DISABLED_HOOKS; asserting one sends a reader
95
- * who is in another to the wrong fix. The wiring cause leads because it is the
96
- * only one the reader can repair in this session, and nothing else reports it.
93
+ * scan-loaded-instructions, a Claude Code older than EVENT_MIN_CLI_VERSION, and
94
+ * the hook switched off in AGENT_SANITIZER_DISABLED_HOOKS; asserting one sends a
95
+ * reader who is in another to the wrong fix. The wiring cause leads because it is
96
+ * the only one the reader can repair in this session, and nothing else reports it.
97
97
  * @param {string} [sessionId] the harness's session identity, so the answer
98
98
  * belongs to THIS session (see instructionsLoadedFile)
99
99
  * @returns {string | null}
@@ -188,7 +188,8 @@ export function cliMain(opts?: {
188
188
  * must be blocked, or null to let the pipeline continue. Hosts use these for
189
189
  * policy the package has no view of (a required workflow step, a project-local
190
190
  * rule); the package ships none.
191
- * @typedef {(input: { tool_name: string | null, tool_input: any, session_id?: string })
191
+ * @typedef {(input: { tool_name: string | null, tool_input: any, session_id?: string,
192
+ * permission_mode?: string })
192
193
  * => string | null | undefined} HostGate
193
194
  */
194
195
  /**
@@ -221,6 +222,7 @@ export type HostGate = (input: {
221
222
  tool_name: string | null;
222
223
  tool_input: any;
223
224
  session_id?: string;
225
+ permission_mode?: string;
224
226
  }) => string | null | undefined;
225
227
  declare const rehydrateRedacted: typeof import("agent-sanitizer/rehydrate").rehydrateRedacted;
226
228
  export {};