agent-sanitizer 2.34.15 → 2.35.0
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/THREAT-MODEL.md
CHANGED
|
@@ -521,15 +521,48 @@ there. The `sgrNote` flag on a `./output` result means "nothing here rose above
|
|
|
521
521
|
note", so a caller can show the quiet line instead of the banner; one warning
|
|
522
522
|
anywhere in the walk clears it.
|
|
523
523
|
|
|
524
|
+
## Provisioned hook binary (supply chain)
|
|
525
|
+
|
|
526
|
+
The hooks can run from a self-contained executable compiled with
|
|
527
|
+
`bun build --compile`, so a session whose `PATH` has no node at all still
|
|
528
|
+
sanitizes (see `plugin/scripts/provision-hook-binary.sh`). That is the
|
|
529
|
+
product's only path that fetches an executable over the network and later runs
|
|
530
|
+
it, so what anchors its trust is worth stating exactly.
|
|
531
|
+
|
|
532
|
+
- **The anchor is in the repo, not on the wire.** `plugin/dist/hooks/hook-binaries.sha256`
|
|
533
|
+
is committed, and the SessionStart provisioner refuses to install any download
|
|
534
|
+
that does not hash to the digest it pins for that platform. A compromised or
|
|
535
|
+
substituted release asset alone therefore cannot be installed: it fails the
|
|
536
|
+
comparison, is deleted, and the launcher keeps degrading loudly through node.
|
|
537
|
+
- **What the digest is worth rests on the compile being reproducible.** The
|
|
538
|
+
committed digests are generated from the committed bundle, and CI recompiles
|
|
539
|
+
and byte-compares them (`build-hook-binaries.mjs --check`), so a manifest that
|
|
540
|
+
does not describe the bundle in the same commit fails before release.
|
|
541
|
+
- **Not covered: an attacker who can write BOTH the repository and the release.**
|
|
542
|
+
There is no signature; the manifest is trusted because it arrives through the
|
|
543
|
+
same reviewed, gated path as the rest of the plugin. Rewriting it is rewriting
|
|
544
|
+
the plugin, which is outside this boundary.
|
|
545
|
+
- **Not covered: re-verification at exec time.** The digest is checked once, at
|
|
546
|
+
install. The launcher then execs
|
|
547
|
+
`${CLAUDE_PLUGIN_DATA}/hook-binary/agent-sanitizer-hooks` on every hook
|
|
548
|
+
invocation without re-hashing it — that would cost a ~100 MB read per tool
|
|
549
|
+
call. Anything able to write inside `CLAUDE_PLUGIN_DATA` can therefore run
|
|
550
|
+
code in the session, which is why the provisioner creates that directory mode
|
|
551
|
+
700 and installs the binary mode 700. A user-owned data directory is the
|
|
552
|
+
assumption; a shared or world-writable one is not supported.
|
|
553
|
+
- **Opting out.** `AGENT_SANITIZER_HOOK_BINARY=0` never downloads and never
|
|
554
|
+
runs a binary, leaving the node path exactly as it was.
|
|
555
|
+
|
|
524
556
|
## Failure posture (`AGENT_SANITIZER_FAIL_OPEN`)
|
|
525
557
|
|
|
526
558
|
Installed as Claude Code hooks, these fail **open**: a hook that could not
|
|
527
559
|
complete lets the guarded action through with a warning in `additionalContext`
|
|
528
560
|
rather than blocking the session. `AGENT_SANITIZER_FAIL_OPEN=0` (or `false`)
|
|
529
561
|
restores the fail-closed verdicts — block, ask, suppress. The posture covers
|
|
530
|
-
every way a hook can fail: the launcher
|
|
531
|
-
|
|
532
|
-
|
|
562
|
+
every way a hook can fail: the launcher finding no runtime (no `node`, and the
|
|
563
|
+
provisioned hook binary missing, unexecutable or failing without a verdict),
|
|
564
|
+
a missing or corrupt bundle, the package never loading, a payload that never
|
|
565
|
+
parsed, and a layer that ran and threw.
|
|
533
566
|
|
|
534
567
|
One carve-out: when the PreToolUse hook itself fails (redactor daemon down,
|
|
535
568
|
package failed to load, a layer threw) and the call is a **write-shaped tool**
|
|
@@ -542,11 +575,12 @@ placeholder-bearing write is never the benign availability case the open default
|
|
|
542
575
|
protects — the model can retry once the sanitizer recovers, or ask the user. The
|
|
543
576
|
check is package-free (a literal-string test on the already-parsed payload), so
|
|
544
577
|
it holds even when the failure IS the missing package. Two accepted gaps: a
|
|
545
|
-
launcher-level failure (no `node`, corrupt bundle
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
578
|
+
launcher-level failure (no `node`, corrupt bundle, a provisioned hook binary
|
|
579
|
+
that is missing, unexecutable or dies without a verdict) never reaches the
|
|
580
|
+
check — the launcher cannot inspect the payload and always warns — and `Bash`
|
|
581
|
+
is excluded even though shell redirection can also persist placeholder text,
|
|
582
|
+
because command strings mention `[REDACTED` benignly far too often for the ask
|
|
583
|
+
to hold precision. All other faults keep the open default, and
|
|
550
584
|
`AGENT_SANITIZER_FAIL_OPEN=0` behavior is unchanged.
|
|
551
585
|
|
|
552
586
|
**The open default is not enforceable against content.** Several of those
|
|
@@ -158,19 +158,23 @@ export function slowHookNotice(
|
|
|
158
158
|
* @param {string} stepName
|
|
159
159
|
* @param {number} elapsedMs
|
|
160
160
|
* @param {number} [thresholdMs]
|
|
161
|
+
* @param {string} [advice] step-specific speedup advice — the default fits the
|
|
162
|
+
* engine install; the hook-binary download passes its own, because telling a
|
|
163
|
+
* user mid-download that uv would help is advice about the wrong step
|
|
161
164
|
* @returns {string | null}
|
|
162
165
|
*/
|
|
163
166
|
export function slowProvisionNotice(
|
|
164
167
|
stepName,
|
|
165
168
|
elapsedMs,
|
|
166
169
|
thresholdMs = SLOW_PROVISION_THRESHOLD_MS,
|
|
170
|
+
advice = "Installing uv makes it faster",
|
|
167
171
|
) {
|
|
168
172
|
if (elapsedMs <= thresholdMs) return null;
|
|
169
173
|
return (
|
|
170
174
|
`agent-sanitizer PERFORMANCE: one-time setup (${stepName}) took ` +
|
|
171
175
|
`${formatSeconds(elapsedMs)}s, over its ${formatSeconds(thresholdMs)}s budget — ` +
|
|
172
176
|
"this is paid once per install, not per tool call, so the session is not slow from here on. " +
|
|
173
|
-
|
|
177
|
+
`${advice}; if it happens on EVERY new session, report it at ${ISSUE_URL}.`
|
|
174
178
|
);
|
|
175
179
|
}
|
|
176
180
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sanitizer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.35.0",
|
|
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": {
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@types/node": "25.9.1",
|
|
55
55
|
"acorn": "^8.18.0",
|
|
56
56
|
"agent-sanitizer": "link:.",
|
|
57
|
+
"bun": "1.3.11",
|
|
57
58
|
"c8": "11.0.0",
|
|
58
59
|
"esbuild": "0.28.1",
|
|
59
60
|
"eslint": "10.4.0",
|
|
@@ -220,6 +221,7 @@
|
|
|
220
221
|
"gen:joining-type": "node scripts/gen-joining-type.mjs",
|
|
221
222
|
"gen:fail-open-lib": "node scripts/gen-fail-open-lib.mjs",
|
|
222
223
|
"gen:node-floor-lib": "node scripts/gen-node-floor-lib.mjs",
|
|
224
|
+
"gen:hook-binaries": "node plugin/scripts/build-hook-binaries.mjs",
|
|
223
225
|
"lint": "eslint .",
|
|
224
226
|
"test:mutation": "node scripts/mutate.mjs",
|
|
225
227
|
"format": "prettier --write .",
|
|
@@ -69,9 +69,12 @@ export function slowHookNotice(hookName: string, elapsedMs: number, thresholdMs?
|
|
|
69
69
|
* @param {string} stepName
|
|
70
70
|
* @param {number} elapsedMs
|
|
71
71
|
* @param {number} [thresholdMs]
|
|
72
|
+
* @param {string} [advice] step-specific speedup advice — the default fits the
|
|
73
|
+
* engine install; the hook-binary download passes its own, because telling a
|
|
74
|
+
* user mid-download that uv would help is advice about the wrong step
|
|
72
75
|
* @returns {string | null}
|
|
73
76
|
*/
|
|
74
|
-
export function slowProvisionNotice(stepName: string, elapsedMs: number, thresholdMs?: number): string | null;
|
|
77
|
+
export function slowProvisionNotice(stepName: string, elapsedMs: number, thresholdMs?: number, advice?: string): string | null;
|
|
75
78
|
/**
|
|
76
79
|
* Write the slow-hook notice to stderr and return it, or return null when the
|
|
77
80
|
* run was within budget (writing nothing, so the quiet path stays quiet).
|