fix(llm-security): hook coverage — pathguard on Edit, trifecta window, pipe-to-shell, provider keys (#9,#10,#12,#13,#11-doc)

#9 pathguard registered matcher Write only, so an Edit/MultiEdit to an existing protected file (settings.json, .env, .ssh, the hooks themselves) bypassed it entirely; matcher now Edit|Write (the script already reads only tool_input.file_path). #10 the primary trifecta detector's 20-entry window counted marker lines, so accumulated markers scrolled a real leg out (false negative); the window now counts tool-call entries. #13 pre-edit-secrets caught bare provider keys only inside a quoted label assignment; added anchored patterns for Anthropic sk-ant, OpenAI sk-proj, fine-grained github_pat_, Google AIza, and JWT eyJ (minimum-length guarded against prose false positives).

#12 the remote-pipe-to-shell block required a shell immediately after the first pipe, so xargs/sudo/tee/env interposition evaded it and the comment falsely claimed xargs was caught; broadened to reach a shell through intermediate segments while leaving shell-OR fallbacks unblocked. #11 (doc only): knowledge/owasp-skills-top10.md claimed pre-bash-destructive blocks persistence commands — it does not; corrected to mark persistence detection as unimplemented/future (the detector itself is deferred to v8). Suite 2004/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
This commit is contained in:
Kjell Tore Guttormsen 2026-07-18 10:36:39 +02:00
commit 8a59d616fb
9 changed files with 337 additions and 6 deletions

View file

@ -270,6 +270,35 @@ function readLastEntries(stateFile, n) {
}
}
/**
* Read a window covering the last n TOOL-CALL entries plus any marker entries
* interleaved within that span.
*
* v7.8.3 #10 fix: the primary trifecta detector previously used
* readLastEntries(stateFile, WINDOW_SIZE), which counts raw lines with no type
* filter. Marker entries (warning/volume_warning/escalation_warning/...)
* appended to the same file diluted the 20-line window and scrolled real
* trifecta legs out a false negative. This helper counts actual tool-call
* entries (no `type` field) and returns the slice from the n-th-last tool call
* onward, keeping interleaved markers so dedup checks (hasRecentWarning) still
* see them.
* @param {string} stateFile
* @param {number} n - number of tool-call entries the window must cover
* @returns {object[]}
*/
function readToolCallWindow(stateFile, n) {
const all = readLastEntries(stateFile, 10_000);
let toolCount = 0;
let start = 0;
for (let i = all.length - 1; i >= 0; i--) {
if (!all[i].type) {
toolCount++;
if (toolCount === n) { start = i; break; }
}
}
return all.slice(start);
}
/**
* Clean up state files older than CLEANUP_MAX_AGE_MS.
* Only called on first invocation per session (when state file doesn't exist yet).
@ -860,7 +889,9 @@ const messages = [];
// --- Trifecta detection (skip for neutral-only and delegation-only calls) ---
if (!(classes.length === 1 && (classes[0] === 'neutral' || classes[0] === 'delegation'))) {
const window = readLastEntries(stateFile, WINDOW_SIZE);
// v7.8.3 #10: count tool-call entries, not raw lines — marker entries must
// not dilute the trifecta window (see readToolCallWindow).
const window = readToolCallWindow(stateFile, WINDOW_SIZE);
const { detected, evidence } = checkTrifecta(window);
if (detected && !hasRecentWarning(window)) {