residoo 0.12.0 → 0.13.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/guard.js +42 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "residoo",
3
- "version": "0.12.0",
3
+ "version": "0.13.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/guard.js CHANGED
@@ -153,13 +153,14 @@ const GUARDED_TOOL_NAMES = new Set(["Bash", "Read"]);
153
153
  */
154
154
  function evaluateToolInput(toolName, toolInput) {
155
155
  if (!GUARDED_TOOL_NAMES.has(toolName) || !toolInput || typeof toolInput !== "object") {
156
- return { block: false, reason: null };
156
+ return { block: false, label: null, reason: null };
157
157
  }
158
158
  const candidate = toolName === "Bash" ? toolInput.command : toolInput.file_path;
159
159
  const label = matchSensitivePath(candidate);
160
- if (!label) return { block: false, reason: null };
160
+ if (!label) return { block: false, label: null, reason: null };
161
161
  return {
162
162
  block: true,
163
+ label,
163
164
  reason: `residoo guard: this looks like a read of ${label}. Blocked before it could be written to the session transcript. ` +
164
165
  `If this is intentional and safe, ask the human to read it themselves, or disable this hook in .claude/settings.json.`,
165
166
  };
@@ -183,20 +184,44 @@ const PROMPT_GUARD_RULES = PATTERNS.filter((r) => r.confidence === "high");
183
184
  * evaluateToolInput above, not just a copy of the same bar.
184
185
  */
185
186
  function evaluatePromptText(promptText) {
186
- if (typeof promptText !== "string" || !promptText) return { block: false, reason: null };
187
+ if (typeof promptText !== "string" || !promptText) return { block: false, label: null, preview: null, reason: null };
187
188
  for (const rule of PROMPT_GUARD_RULES) {
188
189
  rule.re.lastIndex = 0;
189
190
  const m = rule.re.exec(promptText);
190
191
  if (!m) continue;
191
192
  const value = m[0];
192
193
  if (VENDOR_EXAMPLE_VALUES.has(value) || zeroEntropyTail(value)) continue;
194
+ const preview = redact(value);
193
195
  return {
194
196
  block: true,
195
- reason: `residoo guard: this prompt looks like it contains ${rule.label} (${redact(value)}). ` +
197
+ label: rule.label,
198
+ preview,
199
+ reason: `residoo guard: this prompt looks like it contains ${rule.label} (${preview}). ` +
196
200
  `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
201
  };
198
202
  }
199
- return { block: false, reason: null };
203
+ return { block: false, label: null, preview: null, reason: null };
204
+ }
205
+
206
+ /**
207
+ * Writes one structured audit line to stderr for a block decision --
208
+ * CONTRIBUTING.md's own hard rule (rule 3) names `~/.residoo/rotations.json`
209
+ * as "the only file residoo ever writes outside an explicit --seal...
210
+ * nothing else may claim this carve-out," so this is NOT a new file, the
211
+ * same choice `cred`'s own audit trail already made for the same reason
212
+ * (see src/credRun.js). Durability is the operator's choice: redirect the
213
+ * hook's own stderr at launch if you want it kept, same as `cred`.
214
+ * Never the raw matched value -- `preview` is already redact()'d by the
215
+ * caller (rule 4: no raw value in any log line, ever), and PreToolUse
216
+ * decisions carry no value at all, only a path-pattern label.
217
+ */
218
+ function logAuditLine(errOutput, { event, label, preview, sessionId, cwd }) {
219
+ try {
220
+ errOutput.write(JSON.stringify({
221
+ ts: new Date().toISOString(), tool: "residoo guard", event, decision: "block",
222
+ label, ...(preview ? { preview } : {}), sessionId: sessionId || null, cwd: cwd || null,
223
+ }) + "\n");
224
+ } catch { /* stderr write failing is never a reason to fail the hook decision itself */ }
200
225
  }
201
226
 
202
227
  /**
@@ -207,9 +232,11 @@ function evaluatePromptText(promptText) {
207
232
  * response protocol to `output` (default stdout) -- exit code is the
208
233
  * caller's job (bin/residoo.js), this returns the intended process exit
209
234
  * code instead of calling process.exit itself, matching every other run*
210
- * function in cli.js.
235
+ * function in cli.js. Every BLOCK decision also gets one structured line
236
+ * on `errOutput` (default stderr) -- see logAuditLine's own docstring for
237
+ * why stderr, never a file.
211
238
  */
212
- async function runGuard({ input = process.stdin, output = process.stdout } = {}) {
239
+ async function runGuard({ input = process.stdin, output = process.stdout, errOutput = process.stderr } = {}) {
213
240
  const chunks = [];
214
241
  for await (const chunk of input) chunks.push(chunk);
215
242
  const raw = Buffer.concat(chunks.map((c) => (Buffer.isBuffer(c) ? c : Buffer.from(c)))).toString("utf-8");
@@ -231,6 +258,10 @@ async function runGuard({ input = process.stdin, output = process.stdout } = {})
231
258
  if (payload.hook_event_name === "UserPromptSubmit") {
232
259
  const decision = evaluatePromptText(payload.prompt);
233
260
  if (!decision.block) return 0;
261
+ logAuditLine(errOutput, {
262
+ event: "UserPromptSubmit", label: decision.label, preview: decision.preview,
263
+ sessionId: payload.session_id, cwd: payload.cwd,
264
+ });
234
265
  output.write(JSON.stringify({ decision: "block", reason: decision.reason }) + "\n");
235
266
  return 0;
236
267
  }
@@ -238,6 +269,10 @@ async function runGuard({ input = process.stdin, output = process.stdout } = {})
238
269
  const decision = evaluateToolInput(payload.tool_name, payload.tool_input);
239
270
  if (!decision.block) return 0;
240
271
 
272
+ logAuditLine(errOutput, {
273
+ event: "PreToolUse", label: decision.label,
274
+ sessionId: payload.session_id, cwd: payload.cwd,
275
+ });
241
276
  output.write(JSON.stringify({
242
277
  hookSpecificOutput: {
243
278
  hookEventName: "PreToolUse",