residoo 0.10.0 → 0.11.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/README.md +6 -3
- package/package.json +1 -1
- package/src/cli.js +40 -17
- package/src/guard.js +102 -13
- package/src/scan.js +1 -1
package/README.md
CHANGED
|
@@ -51,8 +51,10 @@ into the conversation at all — which also means a long session compacting
|
|
|
51
51
|
away the exact value you pasted days ago can't force you to paste it
|
|
52
52
|
again, since there's nothing to lose. `residoo guard` blocks an obviously
|
|
53
53
|
sensitive file read before it happens (100% recall, 0% false positives on
|
|
54
|
-
its own [scored 81-case corpus](bench/guard/RESULTS.md))
|
|
55
|
-
|
|
54
|
+
its own [scored 81-case corpus](bench/guard/RESULTS.md)) and, via a second
|
|
55
|
+
hook, a secret typed directly into the prompt itself — confirmed against
|
|
56
|
+
Claude Code's own docs to block before the model ever processes it. All
|
|
57
|
+
four are covered in [docs/features.md](docs/features.md).
|
|
56
58
|
|
|
57
59
|
> [!NOTE]
|
|
58
60
|
> gitleaks and trufflehog scan **commits**. residoo scans the **conversation
|
|
@@ -138,7 +140,8 @@ while losing rows, then fixed in public against the classes it was losing
|
|
|
138
140
|
for CI and pre-commit. See [docs/ci.md](docs/ci.md).
|
|
139
141
|
- `residoo watch` / `residoo mcp` / `residoo cred` / `residoo guard`:
|
|
140
142
|
continuous scanning, conversational queries, credential injection
|
|
141
|
-
without pasting, and
|
|
143
|
+
without pasting, and blocking a sensitive file read or a sensitive
|
|
144
|
+
prompt before either happens. See [docs/features.md](docs/features.md).
|
|
142
145
|
|
|
143
146
|
## What it does not do
|
|
144
147
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "residoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Find secrets leaking through your AI coding agent's session history. Zero network calls in the scan path, zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "CloudRoam (https://cloudroam.io)",
|
package/src/cli.js
CHANGED
|
@@ -218,24 +218,47 @@ Cred:
|
|
|
218
218
|
tool, present only when RESIDOO_CRED_ALLOWED_COMMANDS is configured.
|
|
219
219
|
|
|
220
220
|
Guard:
|
|
221
|
-
residoo guard
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
221
|
+
residoo guard one binary, two Claude Code hooks, dispatched on
|
|
222
|
+
the payload's own hook_event_name:
|
|
223
|
+
|
|
224
|
+
PreToolUse blocks an obviously-sensitive file read
|
|
225
|
+
(.env, id_rsa, .aws/credentials, and similar)
|
|
226
|
+
before it can be written to the session
|
|
227
|
+
transcript at all. Narrower than it sounds:
|
|
228
|
+
Claude Code's hooks API can see a proposed Bash
|
|
229
|
+
command or Read path before it runs, but never
|
|
230
|
+
the command's OUTPUT, so this alone cannot catch
|
|
231
|
+
a secret typed into a prompt or one arriving
|
|
232
|
+
through an unrelated command's output.
|
|
233
|
+
|
|
234
|
+
UserPromptSubmit closes exactly that gap: it
|
|
235
|
+
checks the user's own typed prompt against
|
|
236
|
+
residoo's 79 high-confidence rules (never
|
|
237
|
+
--verify, never --ocr -- this hook has no matcher
|
|
238
|
+
and fires on every single prompt, so it must stay
|
|
239
|
+
fast) and can block it before Claude processes it
|
|
240
|
+
at all -- confirmed directly against Claude
|
|
241
|
+
Code's own docs, not assumed. A block ERASES the
|
|
242
|
+
user's whole typed message, so this path is
|
|
243
|
+
extra-conservative: a documented vendor-example
|
|
244
|
+
key or an obvious placeholder is suppressed, same
|
|
245
|
+
as residoo scan itself. Best-effort, not a
|
|
246
|
+
guarantee: per Claude Code's own docs, a slow
|
|
247
|
+
hook here fails OPEN (the prompt goes through
|
|
248
|
+
unscanned) rather than stalling the session,
|
|
249
|
+
consistent with this hook never blocking on
|
|
250
|
+
anything it doesn't recognize.
|
|
251
|
+
|
|
252
|
+
scan/watch/mcp remain the real safety net either
|
|
253
|
+
way -- this is prevention on top of detection,
|
|
254
|
+
not a replacement for it. Add both to
|
|
235
255
|
.claude/settings.json:
|
|
236
|
-
{"hooks":{
|
|
237
|
-
|
|
238
|
-
|
|
256
|
+
{"hooks":{
|
|
257
|
+
"PreToolUse":[{"matcher":"Bash|Read",
|
|
258
|
+
"hooks":[{"type":"command",
|
|
259
|
+
"command":"residoo guard"}]}],
|
|
260
|
+
"UserPromptSubmit":[{"hooks":[{"type":"command",
|
|
261
|
+
"command":"residoo guard"}]}]}}
|
|
239
262
|
|
|
240
263
|
Rotation:
|
|
241
264
|
residoo explain <rule-id> full rotation runbook for one detection rule
|
package/src/guard.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* `residoo guard`:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `residoo guard`: two Claude Code hooks in one binary, dispatched on the
|
|
5
|
+
* payload's own `hook_event_name` field.
|
|
6
|
+
*
|
|
7
|
+
* PreToolUse blocks an obviously-sensitive file read before it happens,
|
|
8
|
+
* instead of finding the leak in the transcript afterward.
|
|
7
9
|
*
|
|
8
10
|
* Scope, stated plainly because it is much narrower than "prevent secrets
|
|
9
11
|
* from leaking": Claude Code's hooks API gives a PreToolUse hook the
|
|
@@ -17,17 +19,53 @@
|
|
|
17
19
|
* and similar) -- it cannot catch a secret typed directly into a prompt, a
|
|
18
20
|
* secret arriving in the output of an otherwise-unremarkable command
|
|
19
21
|
* (curl, a build log), or any file path this pattern list does not name.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
*
|
|
23
|
+
* UserPromptSubmit closes exactly the "typed directly into a prompt" gap
|
|
24
|
+
* named above: Claude Code's own docs (code.claude.com/docs/en/hooks,
|
|
25
|
+
* fetched and read directly, not summarized secondhand) confirm this hook
|
|
26
|
+
* "runs before every prompt and blocks model processing until it
|
|
27
|
+
* completes" -- a real prevention point, not an after-the-fact alert.
|
|
28
|
+
* Verified precisely, not assumed: the payload's `prompt` field carries the
|
|
29
|
+
* exact submitted text (the field is `prompt`, NOT `user_prompt` -- a
|
|
30
|
+
* specific claim to the contrary did not survive this session's own
|
|
31
|
+
* adversarial verification pass), and a JSON response with
|
|
32
|
+
* `"decision": "block"` "prevents the prompt from being processed and
|
|
33
|
+
* erases it from context." Checked against residoo's existing 79
|
|
34
|
+
* high-confidence PATTERNS.js rules ONLY -- never NOISY_PATTERNS, never
|
|
35
|
+
* --verify (a network call), never --ocr (tesseract, and irrelevant to a
|
|
36
|
+
* text prompt anyway) -- because this hook has NO matcher support (it
|
|
37
|
+
* fires on every single prompt in every session, confirmed in the same
|
|
38
|
+
* docs) and must stay fast: Claude Code's own default timeout for this
|
|
39
|
+
* event's command hooks is 30s, down from 600s elsewhere, specifically
|
|
40
|
+
* because "a stuck hook stalls the session." A false positive here also
|
|
41
|
+
* costs more than a PreToolUse block: it erases the user's entire typed
|
|
42
|
+
* message, not one denied tool call, which is why this path additionally
|
|
43
|
+
* runs the same vendor-example/placeholder suppression `residoo scan`
|
|
44
|
+
* itself uses (see scan.js's VENDOR_EXAMPLE_VALUES/zeroEntropyTail) before
|
|
45
|
+
* ever blocking -- a documented AWS example key or an obvious placeholder
|
|
46
|
+
* must never eat a real prompt.
|
|
47
|
+
*
|
|
48
|
+
* Same fail-open timeout reality as scan.js's whole 30-vendor --verify
|
|
49
|
+
* surface, disclosed rather than hidden: per Claude Code's own docs, a
|
|
50
|
+
* command/http/mcp_tool hook (this one) that times out on UserPromptSubmit
|
|
51
|
+
* has its output discarded and "the prompt still reaches Claude" unblocked
|
|
52
|
+
* -- a genuinely slow scan cannot stall the session, but that also means
|
|
53
|
+
* this is best-effort, additive coverage, not an absolute guarantee, the
|
|
54
|
+
* same honesty already applied to --ocr.
|
|
22
55
|
*
|
|
23
56
|
* Fails safe in the direction of NOT blocking on any uncertainty: a
|
|
24
57
|
* malformed hook payload, an unrecognized tool name, or a parse error all
|
|
25
58
|
* fall through to "allow" (no stdout, exit 0) rather than denying a call
|
|
26
59
|
* this module does not understand. The one thing this module must never do
|
|
27
|
-
* is silently hang or crash the agent's turn over a tool call
|
|
28
|
-
* always going to be fine.
|
|
60
|
+
* is silently hang or crash the agent's turn over a tool call -- or a
|
|
61
|
+
* prompt -- that was always going to be fine. `residoo scan`/`watch`/`mcp`
|
|
62
|
+
* remain the actual safety net; this is a narrower, best-effort tripwire on
|
|
63
|
+
* top, not a replacement for them.
|
|
29
64
|
*/
|
|
30
65
|
|
|
66
|
+
const { PATTERNS, redact } = require("./patterns");
|
|
67
|
+
const { VENDOR_EXAMPLE_VALUES, zeroEntropyTail } = require("./scan");
|
|
68
|
+
|
|
31
69
|
// A matched path fragment must be preceded by a path separator or the start
|
|
32
70
|
// of the string, and followed by either the end of the string (the common
|
|
33
71
|
// case for Read's file_path) or a shell metacharacter/whitespace (the case
|
|
@@ -127,12 +165,49 @@ function evaluateToolInput(toolName, toolInput) {
|
|
|
127
165
|
};
|
|
128
166
|
}
|
|
129
167
|
|
|
168
|
+
// High-confidence only, computed once at module load (PATTERNS is a static
|
|
169
|
+
// array): never NOISY_PATTERNS, matching the same elevated bar decodeLine/
|
|
170
|
+
// ocrLine already use in scan.js for content one step removed from a plain
|
|
171
|
+
// file line -- a user's own live-typed prompt deserves at least that same
|
|
172
|
+
// bar, arguably a higher one given what a wrong block costs here (see this
|
|
173
|
+
// file's own docstring).
|
|
174
|
+
const PROMPT_GUARD_RULES = PATTERNS.filter((r) => r.confidence === "high");
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Pure decision function: given a UserPromptSubmit hook payload's raw
|
|
178
|
+
* `prompt` string, decide whether to block. No I/O, fully unit-testable.
|
|
179
|
+
* Suppresses the same way `residoo scan` does (a documented vendor-example
|
|
180
|
+
* key, or an obviously-placeholder zero-entropy tail) before ever blocking
|
|
181
|
+
* -- a false positive here erases the user's entire typed message, not one
|
|
182
|
+
* denied tool call, so this path is deliberately more conservative than
|
|
183
|
+
* evaluateToolInput above, not just a copy of the same bar.
|
|
184
|
+
*/
|
|
185
|
+
function evaluatePromptText(promptText) {
|
|
186
|
+
if (typeof promptText !== "string" || !promptText) return { block: false, reason: null };
|
|
187
|
+
for (const rule of PROMPT_GUARD_RULES) {
|
|
188
|
+
rule.re.lastIndex = 0;
|
|
189
|
+
const m = rule.re.exec(promptText);
|
|
190
|
+
if (!m) continue;
|
|
191
|
+
const value = m[0];
|
|
192
|
+
if (VENDOR_EXAMPLE_VALUES.has(value) || zeroEntropyTail(value)) continue;
|
|
193
|
+
return {
|
|
194
|
+
block: true,
|
|
195
|
+
reason: `residoo guard: this prompt looks like it contains ${rule.label} (${redact(value)}). ` +
|
|
196
|
+
`Blocked before it could be sent. If this is a false positive, rephrase or remove it, or disable this hook in .claude/settings.json.`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return { block: false, reason: null };
|
|
200
|
+
}
|
|
201
|
+
|
|
130
202
|
/**
|
|
131
|
-
* Reads one PreToolUse hook payload from `input`
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
203
|
+
* Reads one PreToolUse OR UserPromptSubmit hook payload from `input`
|
|
204
|
+
* (default stdin) -- distinguished by the payload's own `hook_event_name`
|
|
205
|
+
* common field, a single binary handling both the way Claude Code's own
|
|
206
|
+
* hook registration allows -- decides, and writes the hook's own JSON
|
|
207
|
+
* response protocol to `output` (default stdout) -- exit code is the
|
|
208
|
+
* caller's job (bin/residoo.js), this returns the intended process exit
|
|
209
|
+
* code instead of calling process.exit itself, matching every other run*
|
|
210
|
+
* function in cli.js.
|
|
136
211
|
*/
|
|
137
212
|
async function runGuard({ input = process.stdin, output = process.stdout } = {}) {
|
|
138
213
|
const chunks = [];
|
|
@@ -146,6 +221,20 @@ async function runGuard({ input = process.stdin, output = process.stdout } = {})
|
|
|
146
221
|
return 0; // malformed payload: fail open, never block on something we can't parse
|
|
147
222
|
}
|
|
148
223
|
|
|
224
|
+
// hook_event_name is a documented COMMON field present on every hook
|
|
225
|
+
// event's payload (code.claude.com/docs/en/hooks), not specific to one
|
|
226
|
+
// event -- checked first so a UserPromptSubmit payload (which has no
|
|
227
|
+
// tool_name/tool_input at all) never falls through to evaluateToolInput
|
|
228
|
+
// and is instead routed to its own decision function and its own
|
|
229
|
+
// response shape (decision:"block", not hookSpecificOutput.
|
|
230
|
+
// permissionDecision -- the two events do not share a response schema).
|
|
231
|
+
if (payload.hook_event_name === "UserPromptSubmit") {
|
|
232
|
+
const decision = evaluatePromptText(payload.prompt);
|
|
233
|
+
if (!decision.block) return 0;
|
|
234
|
+
output.write(JSON.stringify({ decision: "block", reason: decision.reason }) + "\n");
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
149
238
|
const decision = evaluateToolInput(payload.tool_name, payload.tool_input);
|
|
150
239
|
if (!decision.block) return 0;
|
|
151
240
|
|
|
@@ -159,4 +248,4 @@ async function runGuard({ input = process.stdin, output = process.stdout } = {})
|
|
|
159
248
|
return 0;
|
|
160
249
|
}
|
|
161
250
|
|
|
162
|
-
module.exports = { evaluateToolInput, matchSensitivePath, runGuard, SENSITIVE_PATH_PATTERNS };
|
|
251
|
+
module.exports = { evaluateToolInput, matchSensitivePath, evaluatePromptText, runGuard, SENSITIVE_PATH_PATTERNS };
|
package/src/scan.js
CHANGED
|
@@ -943,4 +943,4 @@ function emptyResult() {
|
|
|
943
943
|
// so residoo_verify_finding's v1 only supports the single-token vendors below.
|
|
944
944
|
const VERIFIABLE_RULE_IDS = new Set(Object.keys(SIMPLE_VERIFY_FNS));
|
|
945
945
|
|
|
946
|
-
module.exports = { scan, emptyResult, VENDOR_EXAMPLE_VALUES, VERIFIABLE_RULE_IDS };
|
|
946
|
+
module.exports = { scan, emptyResult, VENDOR_EXAMPLE_VALUES, VERIFIABLE_RULE_IDS, zeroEntropyTail };
|