The review reads the pull request's head, held to it by --restricted (record 0138)

Merged
DO
5d ago
+2164-189

Record 0138. Closes #537.

No longer stacked. #527 (0137) and #532 (P-59, P-60) have landed, and main is merged in at 7f42142f. The merge took harness/task.py, record 0137 and 0137's tests and mutants as main has them, ROW included; in tests/test_evals.py it kept 0138's stand-in CLI and reviewed. The diff against main is 0138 alone.

The review Role now reads the repo at the pull request's head, read-only. This follows the Maintainer's decision of 2026-11-04 and his choice that day, "Tarball, allow codeload". 0138 replaces two rows of 0050, "The Engine"'s empty directory and "No tool for any Run", and the one line of 0045 that one redirect alone is followed. Nothing else in either record changes, and both records' Status now says "replaced by 0138".

What changes

PartChange
The Engine (harness/engine.py)The review Intent alone gets --tools Read,Grep,Glob --allowedTools Read,Grep,Glob --permission-mode dontAsk --restricted --setting-sources "", each once, and nothing that widens them. The scrub is set only with no tool or with exactly these; any other call is engine_failed before the CLI starts. The model is told that the head is data
What each flag does (P-59)--restricted is what confines the reads. --setting-sources "" only keeps the head's settings and CLAUDE.md out. Alone, the second held no read: with the scrub set, the CLI drops the permission mode to its default, and the allowed tools, named with no path, read anywhere, including the CLI's own environment. Each flag has its own test and a mutant that drops it
The transport (adapters/github.py)It follows a second named redirect: a commit's tarball, requested by its whole sha, to that repo's and that commit's archive on codeload.github.com, over HTTPS. No token is sent there, the answer is capped at 32 MiB, and there is no second hop (P-60). Live.anonymous() sends no token and can send only that tarball request. The replay reads P-60's GET of codeload and an archive stored as base64
The unpacker (harness/head.py)It checks every member by hand before writing anything. It writes regular files (0600) and folders (0700) only, opened new and never through a link. Links are left out and counted. It refuses devices, FIFOs and unknown types; names that are absolute, contain .., . or an empty segment, aren't UTF-8, sit outside the archive's one folder, or would land outside the folder by their real path; a member named twice; a bad checksum; a non-octal number; an archive cut short or with bytes after its end; more than 20,000 members or 64 MiB. The pax header must name the commit asked for, and the archive must hold one folder named for it
Live RunsThe reader fetches the head at the commit the diff was read at, with its own token under the existing read:task.change grant, and unpacks it next to the Task's files. The run step starts the CLI in it. A head that can't be read stops the reader with head_unreadable, and run.answer never asks a review that has no head
The evaleval-case/1 requires head: 40 lowercase hex characters, with three counter-examples. The fetch reads each case's head with no credential. The run step asks each case inside its head and removes the head afterwards. A head that can't be read is head_unreadable, one of the Engine's faults, and closes the gate. The canary reads the example's head the same way
WorkflowsNo change. No new secret: the cases' repo is public, and the home's token is never sent anywhere else
DocsDesign section 3.5 and the paragraph on hung Runs, the "Nothing is fetched" standard, and the AGENTS.md rows

Trust boundary

  • New input: the head's files become model input. Unchanged files come from the base; changed ones are already in the diff.
  • Measured at the pin: the head's own .claude settings, hooks and CLAUDE.md do nothing, and no read reaches outside the head's folder (P-59).
  • What a hostile file can still do: the same as the diff already could, which is to lead the model to miss a Finding or invent one. The Result still carries codes only.

Owed

  • What codeload's Location holds for a private repo read with an App's token. P-60 read a public repo with no token, and a live Run on a private head waits on this.
  • The cost of a Run with tools, from the next eval run's usage. The tier's turns and cost ceiling are unchanged; each tool call uses a turn.

Verify

  • Local make verify at 7f42142f (after the merge of main): 1582 tests OK (8 expected failures), every check passing. Run with PATH=/usr/bin:/bin:/opt/homebrew/bin, because #545 (#544's fix for the P-59 tests' timeout on macOS) had not landed.

  • S7044, second round (b3c39f9d). The tarball's path quotes the owner and the name whole, urllib.parse.quote(..., safe=""), through head.path_of. A test holds an owner or name with /, %2e%2e or ? to one segment each, and two mutants drop each quote; both are killed.

  • Local make verify at ec6bb520. The only failures were:

    • context, on the record-number gap, which #527 also hits;
    • one test of mine that used os.walk, fixed in bfcc617b.
  • At ac869098. make checks fails only on that gap; test_harness, test_adapters, test_steps and test_evals pass.

  • Mutants. All 54 new ones and the 25 re-pointed ones were killed at ec6bb520. The two fix rounds' new and re-pointed mutants were killed too. Two of them came back FLAKY from the gap and were checked against their named tests by hand.

  • CI at ec6bb520 and bfcc617b. Every failure is the gap:

    • checks;
    • mutant shards 5 and 6 at baseline;
    • one FLAKY mutant in shards 0, 2, 3, 4 and 7.

Reads

Who read what.

  • Argon: not run (quota). Astra read bfcc617 as its stand-in, after an earlier try failed on the network.
  • Muse gave no read twice at ec6bb52, then read bfcc617 on its third try.
  • The Claude review and the security pass stalled twice on API errors, then read bfcc617.

Outcome. Every finding was fixed in ac869098. Two were fixed in part; their rest is #543, "The review's head is held to the commit's tree, file by file, by its blob hash". The fix round was not read again.

ReviewerHeadUnread partsBlockingMajorMinorInfo
astrabfcc617none0500
musebfcc617none0120
claude-reviewbfcc617none0020
claude-securitybfcc617none0011
FindingSeverityCriterionWhereWhatDisposition
astra#1majorPR-5harness/engine.py:129Equals-form options bypass the ban on widening read permissionsFixed in ac869098: an option is read by its name in either form, the CLI's other spellings of the allowed tools are refused, and the Engine refuses --add-dir=/ before it starts; tests and mutants
astra#2majorPR-3harness/head.py:137The unpacker accepts truncated tar endings and ignores trailing membersFixed in ac869098: the end must be two zero blocks and padding alone; one end block, bytes after it and a device after it each refused, with mutants
astra#3majorPR-3harness/head.py:155An archive's commit comment does not establish a complete headFixed in part in ac869098: the model is told what the archive leaves out or changes, and 0138 says so. Holding the head to the commit's tree by blob hash is #543 ("The review's head is held to the commit's tree, file by file, by its blob hash"), which needs a trial of the tree read
astra#4majorPR-3harness/head.py:170Duplicate member rejection applies only to regular filesFixed in ac869098: every member is named once, folders, links and the one folder among them; four cases and a mutant
astra#5majorPR-3harness/read.py:186Failed extraction leaves an apparently usable Task and partial headFixed in ac869098: the reader unpacks the head first, removes it when it fails and writes nothing; a test shows nothing is left, with a mutant
muse#1majorPR-3harness/engine.py:127read_alone accepts equals-form widening and duplicate tool flagsFixed in ac869098, as Astra's #1
muse#2minorPR-3adapters/github.py:336Replay base64 decoding is global and invalid base64 escapes as the wrong errorFixed in ac869098: base64 is read only where a followed redirect led, and one that is not base64 is refused; tests and two mutants
muse#3minorPR-3harness/head.py:169Unpacker lets colliding file and directory members escape as OSError, not RefusedFixed in ac869098: a file and a folder of one name is a name twice, and a file under a file cannot be written where its name says, each a Refused; cases and a mutant
claude-review#1minorPR-3harness/head.py:170The unpacker accepts a folder named twice, and colliding names escape as OSError, not RefusedFixed in ac869098, as the security pass's #1
claude-review#2minorPR-3harness/run.py:101No test notices when the job stops refusing a head path outside the directory it runs inFixed in ac869098, as Astra's #4 and Muse's #3
claude-security#1minorPR-3harness/run.py:101No test holds the check that refuses a head path outside the directory the job runs inFixed in ac869098: a ninth path outside the job's folder, /, ../ or a link out, is refused in the test, with a mutant
claude-security#2infononeharness/head.py:1The head's tarball is git-archive output, which the pull request's own .gitattributes can redact or rewriteFixed in part in ac869098, as Astra's #3; #543 holds the rest

Second Astra read, at 7f42142f (Argon still out of quota; Factory Builder, 2026-11-04), covering everything since bfcc617. Three majors, each reproduced, none a regression and none reachable from outside, since the Engine builds its own arguments and codeload sends the tarball. They go in one follow-on PR after the eval-and-cut hold:

  • harness/engine.py:140. read_alone reads the whole argument list. It passes a -- before the read flags, and a second value after a variadic --tools Read,Grep,Glob, for example Bash.
  • harness/head.py:152. An archive that ends while a pax extended header still waits for its member is unpacked as complete.
  • tests/test_harness.py:483. The forbidden-option test loops over engine.WIDER itself. Removing --mcp-config from it survives.

Argon: still owed (quota).

Bots.

  • SonarCloud S7044, "API Traversal via file content in Live.send()": gone since b3c39f9d, which quotes the path's owner and name; no waiver. On 7f42142f Sonar's gate passes with no open issue on the PR: S7044 and S8786 are both gone.
  • SonarCloud S5131 was #532's stub, waived there; S8786 was #527's harness/task.py, fixed by 0137's ROW as it landed.
  • CodeRabbit skipped: auto reviews are off.

šŸ¤– Generated with Claude Code