agent-sanitizer 2.26.2 → 2.26.3
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 +28 -8
- package/THREAT-MODEL.md +68 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,14 +217,34 @@ import {
|
|
|
217
217
|
} from "agent-sanitizer/claude-hooks/lib/hook-io";
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
The exported set is
|
|
221
|
-
|
|
222
|
-
`
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
220
|
+
The exported set is **curated, not a wildcard**: exactly the subpaths below and
|
|
221
|
+
nothing else. Anything unlisted is refused by the exports map with
|
|
222
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`, so it never becomes a surface this package owes
|
|
223
|
+
compatibility on. `lib/hook-io` in particular is exported because it must be
|
|
224
|
+
_shared_ rather than copied: it owns the lazy-module registry and the CLI-slot
|
|
225
|
+
singleton, and two copies in one bundle double-fire the inlined CLIs.
|
|
226
|
+
|
|
227
|
+
<!-- exports-table: rows are asserted to equal package.json's ./claude-hooks* exports by test/claude-hooks-exports.test.mjs -->
|
|
228
|
+
|
|
229
|
+
| Subpath | What it is |
|
|
230
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
231
|
+
| `claude-hooks` | The `--hook=` CLI dispatcher all four hooks are spawned through |
|
|
232
|
+
| `claude-hooks/pretooluse-sanitize` | PreToolUse orchestrator: invisible-char gate, confusable folding, stego strip, rehydration |
|
|
233
|
+
| `claude-hooks/sanitize-output` | PostToolUse pipeline: Layers 1–4 over tool output, plus the host-extension bag |
|
|
234
|
+
| `claude-hooks/sanitize-user-prompt` | UserPromptSubmit verdict on payload-capable invisible/ANSI content |
|
|
235
|
+
| `claude-hooks/scan-invisible-chars` | SessionStart scan of `CLAUDE.md` / `.claude/` markdown |
|
|
236
|
+
| `claude-hooks/lib/hook-io` | Shared hook I/O: the lazy-module registry, the CLI slot, deadlines, the hookgate marker |
|
|
237
|
+
| `claude-hooks/lib/control-plane` | Bridge to `agent-control-plane-core` and the shared judge-CLI transport |
|
|
238
|
+
| `claude-hooks/lib/authored-content` | Stego + terminal-control stripping of the fields the MODEL authors |
|
|
239
|
+
| `claude-hooks/lib/env-config` | The env-bound secret vocabulary the Layer-4 pre-gate and the redactor client share |
|
|
240
|
+
| `claude-hooks/lib/invisible-alert` | Cross-hook alert state for uncleanable invisible-char injection in instruction files |
|
|
241
|
+
| `claude-hooks/lib/redactor-client` | Client for the long-lived `agent-secret-redactor-daemon` (Layer 4's transport) |
|
|
242
|
+
| `claude-hooks/lib/reveal` | The Layer-2 sidecar that lets the model re-read what the HTML splice removed |
|
|
243
|
+
| `claude-hooks/lib/secret-annotate` | The cheap deterministic Layer-4 pre-gate checks around the daemon call |
|
|
244
|
+
| `claude-hooks/lib/trace` | The opt-in structured trace channel every layer announces itself on |
|
|
245
|
+
|
|
246
|
+
Only `plugin-hooks` itself is unexported under its own name — it is reachable as
|
|
247
|
+
the bare `claude-hooks` entry above.
|
|
228
248
|
|
|
229
249
|
Importing one runs no CLI and reads no stdin. Same stability posture as the
|
|
230
250
|
`_AGENT_SANITIZER_*` variables below: reachable and typed, but the supported
|
package/THREAT-MODEL.md
CHANGED
|
@@ -135,6 +135,74 @@ the model chooses to follow it — and the sentence reporting it is precisely th
|
|
|
135
135
|
instruction not to. A target whose kind cannot be resolved is treated as
|
|
136
136
|
auto-fetched (fail closed).
|
|
137
137
|
|
|
138
|
+
## Layer 4—secret redaction (injected engine)
|
|
139
|
+
|
|
140
|
+
The threat is the reverse of the other layers: not attacker text reaching the
|
|
141
|
+
model, but a credential in tool output (a `.env` cat, a failing curl, a CI log)
|
|
142
|
+
reaching a model that will paste it into the next tool call, a commit, or a
|
|
143
|
+
bug report.
|
|
144
|
+
|
|
145
|
+
**The package bundles no detector.** Layer 4 is an injected
|
|
146
|
+
`redact(text) => {text, found, note?} | null` callback on `./output`, so the npm
|
|
147
|
+
package ships no secret engine and a host may supply its own. The Claude Code
|
|
148
|
+
plugin injects the Python engine from `agent-sanitizer[secrets]`
|
|
149
|
+
(`python/agent_sanitizer/secrets/engine.py`) over a local daemon;
|
|
150
|
+
`plugin/scripts/provision-redactor.sh` installs it at SessionStart.
|
|
151
|
+
|
|
152
|
+
**It is the one fail-closed layer.** A redactor that throws — unreachable
|
|
153
|
+
daemon, engine error — is rethrown from `sanitizeValue` as `CRITICAL: secret
|
|
154
|
+
redaction failed …` (`src/output.mjs`), so the caller suppresses the output
|
|
155
|
+
rather than emit a value nothing vetted. That is distinct from an engine that
|
|
156
|
+
was never provisioned: there the hooks' posture applies, passing the output
|
|
157
|
+
through with a loud warning by default and suppressing it under
|
|
158
|
+
`AGENT_SANITIZER_FAIL_OPEN=0`. Either way the gap is announced, never silent.
|
|
159
|
+
|
|
160
|
+
**Lone surrogates are normalized to U+FFFD before the redactor sees the text.**
|
|
161
|
+
A secret split by an interposed lone surrogate renders as contiguous to the
|
|
162
|
+
model but arrives broken at the redactor, so without normalization a
|
|
163
|
+
reconstituted secret survives redaction. Both redact-input paths share one
|
|
164
|
+
normalizer so they cannot drift.
|
|
165
|
+
|
|
166
|
+
**Detection.** detect-secrets is the single oracle — its bundled plugins plus
|
|
167
|
+
gitleaks-sourced ones for formats it lacks — extended with a regex
|
|
168
|
+
for the unquoted `key=value` shapes `KeywordDetector` misses, PEM block
|
|
169
|
+
collapse, cross-line reassembly of a secret split across lines, and exact-match
|
|
170
|
+
redaction of caller-supplied env-var **values**. Each hit becomes
|
|
171
|
+
`[REDACTED: <label>]` and its label joins `found`.
|
|
172
|
+
|
|
173
|
+
**The engine discovers nothing about its environment.** Every
|
|
174
|
+
environment-specific input — which env-var values to redact, the invisible
|
|
175
|
+
charset, whether the text is web ingress — arrives through `RedactorConfig`; the
|
|
176
|
+
engine never reads `os.environ`. Passing values rather than names is
|
|
177
|
+
load-bearing for the daemon, which serves many sessions and must redact the
|
|
178
|
+
_requester's_ keys. The invisible charset is sourced from the same SSOT Layer 1
|
|
179
|
+
uses (and raises if that dependency is missing rather than silently using a
|
|
180
|
+
partial set): a key spliced with a code point one layer omits would otherwise
|
|
181
|
+
escape both.
|
|
182
|
+
|
|
183
|
+
**Precision over recall, deliberately.** Redacting a UUID, a content digest, a
|
|
184
|
+
timestamp, a version, a filesystem path, a public endpoint URL, a `$VAR`
|
|
185
|
+
reference, a documentation placeholder or a markdown code span would delete text
|
|
186
|
+
the model needed, so each is filtered out before redaction, and an env value
|
|
187
|
+
shorter than `min_secret_len` (16) is treated as a test stub rather than a key.
|
|
188
|
+
Two config switches move the trade-off where the context justifies it:
|
|
189
|
+
`web_ingress` disables the name-based benign skips for attacker-controlled text,
|
|
190
|
+
and `high_confidence` drops the fuzzy keyword/field-value detectors for source
|
|
191
|
+
scans, where secret-shaped names appear legitimately.
|
|
192
|
+
|
|
193
|
+
**Redaction stays reversible for editing, never for the model.**
|
|
194
|
+
`redact_map` returns placeholder ↔ original pairs with offsets, which
|
|
195
|
+
`./rehydrate` uses to re-anchor a model `Edit` composed from the redacted view
|
|
196
|
+
back onto the real bytes. If the input already contains the private-use
|
|
197
|
+
sentinels the map machinery reserves, it returns `{"unmappable": …}` rather than
|
|
198
|
+
risk mis-pairing a placeholder with the wrong secret.
|
|
199
|
+
|
|
200
|
+
**Ordering.** Layer 4 runs after Layers 1–2 have removed bytes, and is re-run on
|
|
201
|
+
the post-deletion text whenever Layer 5 deletes a span — a deletion can
|
|
202
|
+
reconstitute a secret the first pass never saw intact. On `Bash.command` it runs
|
|
203
|
+
before `sanitizeAuthoredContent`, which is the assumption the confusable-folding
|
|
204
|
+
soundness argument below relies on.
|
|
205
|
+
|
|
138
206
|
## Confusable folding (tool input)
|
|
139
207
|
|
|
140
208
|
`./confusables` folds look-alike glyphs in tool-call **input** fields (paths,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sanitizer",
|
|
3
|
-
"version": "2.26.
|
|
3
|
+
"version": "2.26.3",
|
|
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": {
|