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 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)). All four are
55
- covered in [docs/features.md](docs/features.md).
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 pre-read blocking. See [docs/features.md](docs/features.md).
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.10.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 a Claude Code PreToolUse hook that blocks an
222
- obviously-sensitive file read (.env, id_rsa,
223
- .aws/credentials, and similar) before it can be
224
- written to the session transcript at all --
225
- prevention, not just detection. Reads one hook
226
- payload from stdin, writes a deny decision to
227
- stdout only when it matches; never blocks on
228
- anything it doesn't recognize. This is narrower
229
- than it sounds: Claude Code's hooks API can see a
230
- proposed Bash command or Read path before it
231
- runs, but never the command's OUTPUT, so this
232
- cannot catch a secret typed into a prompt or one
233
- arriving through an unrelated command's output --
234
- scan/watch/mcp remain the real safety net. Add to
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":{"PreToolUse":[{"matcher":"Bash|Read",
237
- "hooks":[{"type":"command",
238
- "command":"residoo guard"}]}]}}
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`: a Claude Code PreToolUse hook that blocks an obviously-
5
- * sensitive file read before it happens, instead of finding the leak in the
6
- * transcript afterward.
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
- * `residoo scan`/`watch`/`mcp` remain the actual safety net; this is a
21
- * narrower, best-effort tripwire on top, not a replacement for them.
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 that was
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` (default stdin), decides,
132
- * and writes the hook's own JSON response protocol to `output` (default
133
- * stdout) -- exit code is the caller's job (bin/residoo.js), this returns
134
- * the intended process exit code instead of calling process.exit itself,
135
- * matching every other run* function in cli.js.
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 };