Repository navigation
fix: 81 validated findings from the 2026-10-11 bug hunt - #316
Merged
Merged
Conversation
A refused batch card marked every call denied, so an auto-allowed check that was never shown became blocked and stopped gating completion. Only listed calls are now denied; unlisted siblings are recorded as not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Budget-skipped and cancelled-before-execution calls were recorded as failed runs, invalidating passed checks and marking skipped checks failed. They are now flagged not-run and skip check recording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildToolDefs ranged over the registry map, so each run sent tools in a new order and missed the provider prompt cache. Definitions are now name-sorted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…me cap Appending an evidence note could push a check description past the 200-rune cap the resume parser enforces, dropping the whole plan on continue. The combined description is now clamped to that cap at write time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The replacement was validated in isolation, so it could reuse a sibling check's id and the resume parser then dropped the plan. Such replacements are now rejected with a retryable error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The revision reason built from an unbounded justification could exceed the 240-rune cap the resume parser enforces, dropping the plan on continue. The justification is now clamped so the reason always fits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heck A fresh replacement check left its step done with no evidence, unlike every other evidence-resetting path. The step now reopens to in_progress. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…never leak argv0 used strings.Fields, so a quoted env assignment value with spaces leaked its second word as the program name into tool_call_started events. Words are now split honouring quotes and escapes; an unterminated quote yields nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generic matcher missed SecretAccessKey/SessionToken (STS JSON) and the YAML colon form AWS_SECRET_ACCESS_KEY: value; it now lists both key names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the Bearer header form was covered, so base64(user:password) from curl -v and HTTP dumps persisted verbatim. A Basic token is now redacted when it decodes to a printable user:password pair; the perf-test reference includes the new pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Bearer pattern required a colon right after Authorization, so the quoted header-object form used in fetch()/axios code and HTTP tool arguments leaked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generic key list covered access_token but not refresh_token, so opaque refresh tokens from OAuth token responses were persisted verbatim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The known-value replacer was built in map order and strings.Replacer takes the first matching argument, so a registered prefix could leave a longer secret's tail in clear. Forms are now sorted longest first, deterministically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local credential check required an algorithm label, so unlabelled BEGIN PRIVATE KEY and BEGIN ENCRYPTED PRIVATE KEY blocks scanned BENIGN. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dry run filtered sessions on UpdatedAt only while Store.Cleanup skips pinned ones; both now share session.CleanupExpired. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dry run clamped logging.max_age_hours at 36500 while the sweep clamps at 87600; the preview now calls maintenance.RuntimeLogExpiredPreview, which shares the policy, clamp and cutoff with PruneAtStartup. Adds the cleanup RED tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sweepAudit only removed names ending in .json, so <id>.json.corrupt-<ts> copies (same user messages) outlived audit_max_age_days. Sweep and dry run now share one audit selection that includes them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
isSource used a raw string prefix, so a look-alike host extending the fetched host name was dropped from untrusted_resources; matches now require equality or a '/', '?' or '#' boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its 99-byte label exceeded the runtime log's 96-byte metadata cap and was dropped; it is now telegram_allow_all_no_allowlist, and a test checks every mapped surface label survives the runtime log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pass prefiltered on "basic" alone and ran submatch extraction over the whole text with an uncapped token, making RedactSecrets 2-3x slower on large input. It now needs "authorization" too, caps the token at 1000 bytes (longer runs are redacted whole) and takes submatches per match only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The AWS session token in the X-Amz-Security-Token header and presigned-URL query parameter matched no pattern; security_token joins the generic key list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The approver's approval_expired frame was ignored by the run sink, leaving a dead pending entry and status stuck at waiting_approval. Treat it like an ack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trailing-hour filter reused the slice in place but only stored it back when over the limit, duplicating a live timestamp. Always store the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
newServeAgent returned on sandbox failure without stopping the MCP servers it had started, and skipped the guard on odek.New failure. One fail path now releases everything acquired so far. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gistry subagentRunKeysForSession read serveRun.SessionID holding only the registry lock while record() writes it under run.mu. Read each run under its own lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shutdown closed WebSockets but never cancelled REST runs, so a run blocked on an approval outlived the drain and skipped its sandbox/MCP cleanup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WebUI session title was cut to 60 bytes before save-time redaction, so a key straddling the cut survived as a fragment. Redact first, as REST runs do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
serve forced the sandbox on unless --no-sandbox was passed, ignoring "sandbox": false in trusted config and ODEK_NO_SANDBOX=1. It now uses the same intent rules as run, including ODEK_REQUIRE_SANDBOX=1 rejecting opt-outs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cancel re-arms the approver, so a prompt from the draining loop after a run was cancelled waited out the approval timeout unseen. REST runs now Close the approver, which denies every later prompt immediately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The REST job listing returned the raw command head while the bg_job frame redacted it, and both clamped before redacting. One helper now redacts the whole command, then clamps, for every client surface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one-token rule used the raw word count, so a query with one usable token plus a short or repeated word never qualified a session by message content. It now uses the count of distinct usable tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to the corrupt-index rebuild: a directory planted at index.json served the cached index, so a lazy save skipped its write and the failure went unnoticed. A non-regular index path now reads as empty again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…safe An unsafe merge-on-write combination made AddFact fail, dropping a fact that passed the per-fact checks. The merge is now skipped, the existing entry is left untouched and the new fact is added as its own entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cution git grep ran its pager option through the shell but only the repository arming check was consulted, so the command classified safe. The pager option is now detected in every spelling (fused, clustered, abbreviated long form). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
send-email was unknown to every git adapter, so --sendmail-cmd, the *-cmd options and a path-valued --smtp-server ran programs unprompted, and mailing local files was safe. They are now code_execution, and send-email/imap-send carry network_egress plus network_upload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
git credential fill and the credential-<helper> verbs print stored plaintext credentials but classified safe, while cat ~/.git-credentials is system_write. They now classify system_write like the direct file read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gpg sits on the read-only safe list, so --export-secret-keys and --export-secret-subkeys printed private key material unprompted. Both (and their abbreviations) now classify system_write like reading the key files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecution Only literal .shell/.system/.read/.load were detected, so '|cmd' arguments of .import/.once/.output, abbreviated .sh/.sy and the edit() SQL function ran programs while classified safe or local_write. A dot-command parser now flags all of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sqlite3 executes dot-commands from stdin, but piped or redirected scripts were never inspected, so echo '.shell …' | sqlite3 and sqlite3 db < cmds.sql were safe. Redirected and non-literal piped input is now code_execution and static pipes are scanned for program launchers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sqlite3 write-target adapter only read .output/.once/.save/.backup, so
writefile('/etc/…') and VACUUM INTO wrote anywhere while classified safe. Their
literal paths are now write targets (non-literal paths fail closed), also in
static scripts piped to sqlite3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zip -m/--move and 7z -sdel delete every archived source, like tar --remove-files, but classified local_write and were auto-allowed. They are now destructive in every spelling (clusters, long-option prefixes, any case). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mv operands were checked only for sensitive paths, and the home directory itself is not one, so mv ~ /tmp/x was auto-allowed while mv -t /tmp/x ~ was system_write. An mv source naming a home directory or its ancestor now escalates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rectory Only the literal operand was path-checked, so chmod -R 777 ~ and gzip -r ~ were local_write although they rewrite ~/.ssh and the rc files. chmod, chown, chgrp, chattr, setfacl and the in-place compressors now escalate to system_write when an operand names a home directory or its ancestor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alias was a safe builtin and its body was never analysed, yet sh expands aliases on later lines, so alias ls='rm -rf ~' followed by ls ran unprompted. Each NAME=BODY definition is now analysed as its body, including the denylist scan. The curl|sh case of the original test is pinned at code_execution, the class that pipeline has on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only --output was a write target for format-patch, so -o and --output-directory wrote patch files into any directory while classified safe. Both spellings (fused, clustered, abbreviated) are now write targets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… repo git apply had no write-target adapter, so --unsafe-paths --directory=/etc applied patches anywhere while classified safe. --unsafe-paths now floors the command at system_write and its --directory is judged by the path rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pins option terminators, negations and next-word values for the mv, zip, 7z, sqlite3, send-email and git apply adapters, and summarises the new rules in AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion Classifying an alias body at its definition missed a benign body completed by its arguments (alias ls=rm; ls -rf ~) and over-escalated unused aliases. The analysis now keeps an alias table across segments and nested payloads and classifies each use as body plus remaining words, alongside the literal command; escaped-space values read like quoted ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only --export-secret-keys and --export-secret-subkeys were covered, so the SSH format export of the same private key stayed safe. Every --export-secret-* option now classifies system_write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the leading literal of the path argument was read, so a concatenation
('/tmp/'||'../etc/x') was judged by its harmless prefix, and a newline or
comment between VACUUM and INTO hid the target. Arguments must now be exactly
one literal (else unknown), and comments and line breaks are skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Write dot-commands were read only as exact .output/.once/.save/.backup arguments with the first operand unquoted, so .log/.clone/.open, abbreviations, quoted or second operands and static scripts piped into sqlite3 escaped the path rules. One parser now feeds every operand to the write-target check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
odek | ee63ac2 | Commit Preview URL Branch Preview URL |
Oct 11 2026, 10:55 AM |
… guard An alias used after time [-p] was not expanded, and the guard that stops an alias from re-expanding its own name leaked into eval and sh -c payloads parsed afresh inside the body. The command word now skips time/! prefixes and the guard covers only the expansion text itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d .archive sqlite3 resolves one-letter abbreviations such as .o (output), and .trace and .archive write files, but the parser required two letters and lacked those commands, so .o /etc/x and .trace /etc/x were safe. Every prefix and both commands are now write targets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d quoted schemas A line break before writefile's parenthesis or a quoted schema name between VACUUM and INTO kept the target unread, so both were safe. Line breaks are accepted, every SQL quoting style is read as a schema name, and a writefile or VACUUM ... INTO that still cannot be read fails closed as unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Satisfies staticcheck QF1001; no behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Redaction swallows the rest of a URL after a token, so an injected URL on the fetched page's host could collapse into the source's redacted form and drop out of untrusted_resources. Compare this turn's raw strings; a persisted source that lost text to redaction excludes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The raw-string source match left redacted resources from persisted ingest records unable to match any source, so a fetched token-bearing URL naming itself was flagged. Raw resources match raw sources; persisted resources match the redacted sources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the 2026-10-11 bug hunt at 3e3ee53: eight area hunters each wrote a failing
TestRED_*test per finding, and an independent Haiku agent replayed and adversarially validated every one. 82 findings → 69 confirmed, 12 plausible, 1 rejected (a leading/in Telegram is the documented command convention). All 81 remaining findings are fixed, one commit per finding, each with its RED test kept as the regression test.Every area then got an adversarial Sonnet review of its fix branch; every real issue those reviews found is fixed in follow-up commits on this branch (listed below).
High severity
git grep -O,git send-email --smtp-server=<prog>/--sendmail-cmd, sqlite3 pipe dot-commands /edit()/.shellon stdin,zip -m/7z -sdel, andalias(now tracked by use: a later command that names an alias is classified as its expansion) were all under-classified.SecretAccessKey/SessionTokenwere not redacted.By area
writefile()/VACUUM INTO/write dot-commands are write targets; recursive chmod/compress andmvof$HOME→ system_write;git apply --unsafe-paths,format-patch -ocheck_replacecan no longer write a plan the resume parser drops; quote-awareargv0index.jsonrebuilt from session files; symlink-escape episode taint; stale episode index; unsafe merged facts; rerank actually applied; embedder refit race; quarantine listing sanitized\, lone*/backtick; commands split on any whitespace; re-enable no longer fires a catch-up run/api/jobsandbg_jobredact before truncatingconfig_viewaccepts real argument JSON;http_requestwith mutating methods/credential headers → network_upload; patch diff/CRLF; symlinked search roots;**char classes;json_querychained indices; diff trailing newline; bgproc 143/137 after stopplanning.remindmerged; secrets.env tab comments; MCPMcp-Session-Id(dropped on cross-host redirect),notifications/initializedon stdio, ping replies on a separate bounded queue; REPL escape sequences; project skill promote requires--force; unknowntrust_levelfails closed;file://refs percent-encodedrefresh_token,X-Amz-Security-Token, deterministic longest-first known-value replacement (with a bounded Basic-auth pass); PKCS#8 key detection; cleanup dry-run shares the real sweep's rules; audit source match on path boundaries; quarantined audit copies age outBehaviour changes worth noting
odek serve --no-sandboxwithODEK_REQUIRE_SANDBOX=1now refuses to start (matchesrun).sqlite3 db < schema.sqland… | sqlite3 dbnow prompt (stdin content cannot be scanned at classification time).alias dc="docker compose"; dc upprompts).TestRecallRerank, three browser URL tests,TestEscapeMarkdown_AllReservedChars,TestWithHTTPClientInjectsTransport.Review follow-ups (adversarial Sonnet review per area)
X-Amz-Security-Tokenadded.gpg --export-secret-ssh-key, sqlite literal-only write paths and SQL comment/newline normalisation, piped sqlite write dot-commands, thentimeprefixes, nested-guard scoping,.o/.trace,writefilenewline and quoted schema names.Coverage (before → after, per touched package)
Test plan
go build ./...,go vet ./...,gofmt -lcleango test -count=1on every package;internal/dangerwithHOME=/home/usergo test -raceon every changed packageODEK_E2E=true … -run TestMCPClientE2E_🤖 Generated with Claude Code