Skip to content

Seven fixes from a Claude Code 2.1.220 / Opus 5 payload audit - #1

Closed
elhoim wants to merge 15 commits into
mainfrom
upgrade/2026-07-26
Closed

Seven fixes from a Claude Code 2.1.220 / Opus 5 payload audit#1
elhoim wants to merge 15 commits into
mainfrom
upgrade/2026-07-26

Conversation

@elhoim

@elhoim elhoim commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Seven fixes found by auditing the install payload against Claude Code 2.1.200–2.1.220 and the July 2026 Claude API release notes. Each was developed on its own branch and merged here; every one is verified by execution rather than inspection.

Critical

context: fork skills were silently detached. 2.1.218 changed skills declaring context: fork to dispatch as background agents by default. Seven skills declare it and all of them return results inline to the caller, so their output was arriving as a task notification instead of in the invoking turn. Pins background: false on each.

cat /proc/self/environ auto-allowed. CREDENTIAL_PATHS guards ~/.ssh, ~/.aws/credentials, GPG keyrings and .env, but procfs reaches the same secrets by another route. Because cat is in SEARCH_TOOLS, classifyCommand() took the read-only-command allow branch and printed the whole environment with no prompt. Adds a procfs entry so those reads defer to the permission prompt.

Fixes

MCP and ToolSearch results were never injection-scanned. Safety.hook.ts documents the intended wiring — "the matcher only routes WebFetch/WebSearch + the qualifying mcp__ names here" — but the matcher only ever carried the WebFetch/WebSearch half, leaving the ToolSearch and mail/drive/calendar branches of isAttackerWritableSource unreachable. Merges the matcher into one alternation covering all four classes, and adds dropbox to the gate for the same reason drive is already on it.

CURRENT.opus still pinned claude-opus-4-8. Opus 5 shipped 2026-07-24. This is the drift models.ts exists to prevent and the exact case its own header cites. Applied via UpdateModels.ts --apply.

Subagent nesting guidance was inverted. The default moved 5 → 1 → 3 across 2.1.217–2.1.219; the docs still said subagents cannot nest. Also records the two per-session ceilings (200 spawns, 200 web searches) that a long fan-out actually reaches.

Stale version pointer. The row that teaches how to resolve the Algorithm version named 8.1.0 against a LATEST of 8.4.0.

Feature

Interceptor/Tools/Zoom.ts — measured zoom for appearance verification. Images are downscaled to ~1568px on the long edge before a model sees them, so on a 2000px page a 40px logo arrives as a smudge, and cropping the delivered screenshot cannot recover it. Zoom crops from the full-resolution original and magnifies the region to the budget. Nearest-neighbour, so it magnifies without inventing detail; never downscales; prefers magick and falls back to ffmpeg.

Verification

  • Classifier: 16 cases through the real classifyCommand() — 8 procfs/credential shapes now neutral, 8 ordinary reads (/proc/cpuinfo, /proc/meminfo, /proc/sys/*, README.md) still allow. No regression.
  • Matcher: compiled and asserted against real tool names — routes WebFetch/WebSearch/ToolSearch/Gmail/Drive/Calendar/Dropbox, skips Bash/Read/Edit/Agent/Indeed/Kiwi. Matcher and in-code gate agree on every case.
  • Frontmatter: all 7 SKILL.md files re-parsed; background lands inside the frontmatter block in each.
  • Zoom: 60×44 crop of a 2000×1200 source → 1568×1150 at 26.13×, marker colour preserved, output inspected visually. All 8 guard paths (out-of-bounds, zero area, negative origin, missing arg, missing file, no args, bad budget, oversized region) exit as intended.
  • Registry: pinnedModelForEffort("high")claude-opus-5; isCurrent("claude-opus-4-8") → false.

Not included, deliberately

  • sandbox.network.strictAllowlist — no sandbox block is configured, so it would be inert; enabling sandboxing needs a real host allowlist and is a posture decision, not a bug fix.
  • A SubagentStop hook — looked like a gap, isn't. AgentInvocation.hook.ts records that the native payload wrote "unknown" for 5,844 of 5,846 events, which is why lifecycle tracking lives on PreToolUse/PostToolUse of the Agent tool. Wiring it would re-add a known-bad source.
  • Un-flattening the reasoning-effort ladder. Opus 5 makes effort its primary control and rejects thinking: disabled above high, so the uniform-high mapping is worth revisiting — but it is a stated directive in models.ts, not an oversight, so it stays for the owner to decide.
  • Retiring the mode-era files (parameter-schema.md, target-types.md, eval-guide.md) still describing abolished modes — deletions, left for the owner.

danielmiessler and others added 15 commits July 23, 2026 14:54
Claude Code 2.1.218 changed skills declaring `context: fork` to dispatch
as detached background agents by default, opt-out via `background: false`.

Research, Council, Ideate, Evals, SystemsThinking, RootCauseAnalysis and
Knowledge all declare `context: fork` and their workflows return results
inline to the calling thread. Under the new default their output arrives
as a task notification instead, so the invoking turn sees nothing.

Pin the previous behaviour explicitly rather than inheriting a default
that changed underneath the skill.
CREDENTIAL_PATHS guards ~/.ssh keys, ~/.aws/credentials, GPG keyrings and
.env files, but procfs reaches the same secrets by another route: a
process's environ holds its API keys and tokens, and mem/maps expose its
address space.

Because `cat` is in SEARCH_TOOLS, classifyCommand() took the
read-only-command allow branch for `cat /proc/self/environ` and returned
`allow` — printing every secret in the environment with no prompt.

Add a procfs entry to CREDENTIAL_PATHS so those reads return `neutral`
and defer to the native permission prompt. Covers literal pids, `self`
and the shell forms ($$, $PID, ${PID}) that reach the same file.
Ordinary procfs reads (cpuinfo, meminfo, loadavg, /proc/sys/*) are
unaffected and still auto-allow.
The nesting default has moved twice in three releases: 5 until 2.1.217,
1 in 2.1.217, and 3 from 2.1.219. Guidance stating that subagents cannot
spawn subagents steers away from a pattern the platform now enables by
default.

Record the current depth-3 default with the flag to flatten it, and add
the two per-session ceilings (200 subagent spawns, 200 WebSearch calls)
that a long fan-out session can actually reach — a denied spawn late in a
session is usually the session budget rather than the concurrency cap.
Claude Opus 5 shipped 2026-07-24 and is the default Opus model. The
registry still pinned claude-opus-4-8 — the exact drift this file exists
to prevent, and the one its own header cites as the cautionary example.

Applied via `UpdateModels.ts --apply opus claude-opus-5`. Blast radius is
small: consumers that take the tier ALIAS were already auto-resolving, so
only pinnedModelForEffort() and the drift scanner change behaviour.

Left alone deliberately: the dated probe records in the architecture doc
and ALGORITHM/changelog.md are history and should keep naming the model
that actually ran, and cost-aggregator's includes("opus") fallback already
returns correct pricing since Opus 5 holds Opus 4.8's $5/$25 rate.
… scan

Safety.hook.ts documents the intended wiring at isAttackerWritableSource:
"the settings.json PostToolUse matcher only routes WebFetch/WebSearch +
the qualifying mcp__ names here", with the in-code gate as the defensive
backstop.

The matcher only ever carried the WebFetch/WebSearch half. The ToolSearch
and mcp mail/drive/calendar branches of that predicate were unreachable,
so third-party-authored text arriving over those tools was never given the
data-not-instructions framing or the injection-shape scan.

Merge the two matcher entries into one alternation covering all four
classes. The in-code gate still does the precise filtering, so unrelated
MCP servers cost one neutral passthrough and nothing more.

Also add dropbox to the gate: file content pulled from a sync service is
attacker-writable for the same reason drive already is (shared folders,
file requests).
The row that teaches how to resolve the Algorithm version named 8.1.0
while ALGORITHM/LATEST reads 8.4.0 — three versions of drift in the one
place a reader looks up the answer. Line 26 of the same file was already
correct, so this was a single missed edit rather than a systemic lag.
Images are downscaled to ~1568px on the long edge before the model sees
them. On a 2000px-wide page a 40px logo arrives as a smudge, and cropping
the delivered screenshot cannot recover it — the detail was destroyed
upstream. That is the failure mode behind the appearance-vs-existence
rule: a screenshot proves an element exists, not what it looks like.

Zoom.ts takes absolute coordinates in the ORIGINAL image, crops from the
full-resolution source, and magnifies that region to the image budget, so
the pixels spent land on the thing under test. Nearest-neighbour on the
upscale, so it magnifies without inventing detail. It never downscales: a
region already past the budget is passed through at native size.

Prefers `magick` (the binary Capture.sh and VerifyImageProbe.ts already
use) and falls back to `ffmpeg` so it works on hosts without ImageMagick.
Out-of-bounds, zero-area and negative-origin crops exit 2 with the source
dimensions in the message rather than emitting a misleading image.
@elhoim

elhoim commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

Superseded — split by category and sent upstream to danielmiessler/LifeOS: danielmiessler#1654 (security), danielmiessler#1655 (docs), danielmiessler#1656 (platform drift), danielmiessler#1657 (Interceptor zoom). The delegation wording was corrected before resubmission per the note on danielmiessler#1569.

@elhoim elhoim closed this Jul 26, 2026
@elhoim

elhoim commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

Full index of the upstream PRs this work became — six in total against danielmiessler/LifeOS.

Split out of this PR (the original seven commits, regrouped by category):

Follow-on work from the same session, not part of this PR's commits:

Two corrections were made before resubmission: the delegation wording was made neutral on over-cap spawn behaviour per the note on danielmiessler#1569, and Zoom.ts's magick backend — shipped unverified here — was tested and confirmed byte-identical to the ffmpeg path (RMSE 0).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants