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 +2 -0
- package/THREAT-MODEL.md +3 -2
- package/claude-hooks/lib/invisible-alert.mjs +21 -9
- package/claude-hooks/pretooluse-sanitize.mjs +8 -4
- package/package.json +1 -1
- package/types/claude-hooks/lib/invisible-alert.d.mts +4 -4
- package/types/claude-hooks/pretooluse-sanitize.d.mts +3 -1
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
|
|
381
|
-
and `scan-loaded-instructions` switched off in
|
|
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
|
|
322
|
-
* switched off in AGENT_SANITIZER_DISABLED_HOOKS; asserting one sends a
|
|
323
|
-
* who is in another to the wrong fix. The wiring cause leads because it is
|
|
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
|
|
338
|
-
"
|
|
339
|
-
"the
|
|
340
|
-
"
|
|
341
|
-
"
|
|
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
|
|
599
|
-
// gate keyed on the session (a once-per-session checkpoint)
|
|
600
|
-
// sessions apart without
|
|
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.
|
|
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
|
|
94
|
-
* switched off in AGENT_SANITIZER_DISABLED_HOOKS; asserting one sends a
|
|
95
|
-
* who is in another to the wrong fix. The wiring cause leads because it is
|
|
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 {};
|