Write CI scratch files to $TMPDIR instead of the sandbox's read-only /tmp (#4063)

Two CI-facing recipes in this repo write scratch files to a path the
agent sandbox no longer allows, so each fails at its first redirect.
This points them at `${TMPDIR:-/tmp}` — writable inside the sandbox, and
still `/tmp` for a local run.

Since tend 0.2.3 the agent runs inside a sandbox with an allowlisted
write set, and root `/tmp` is not in it. This repo picked that up with
the 0.2.5 regeneration in #4057, merged yesterday at 10:37. Verified
from inside a tend session on this repo:

```
$ sh -c 'echo x > /tmp/codecov.json'
sh: 1: cannot create /tmp/codecov.json: Read-only file system
```

The affected recipes:

- **`tests/CLAUDE.md` → Coverage Investigation** — `curl … >
/tmp/codecov.json` plus the two `jq` reads of it. This block exists for
exactly the case that now breaks: it names "running in CI, where `task
coverage` isn't installed" as one of its two reasons to query Codecov
directly, and `.claude/skills/running-tend/SKILL.md` sends every CI
session investigating a `codecov/patch` failure here.
- **`.claude/skills/running-tend/SKILL.md`** — the
`code-coverage-report` artifact download (`/tmp/coverage.zip`, `unzip -d
/tmp/coverage`) and the weekly statusline cache-check's stdin fixture
(`/tmp/statusline-input.json`).

Both rewritten forms were run in this session before committing: the
codecov query against `#4058`'s base→head pair returns the real per-file
patch table, and `jq -n … > "${TMPDIR:-/tmp}/statusline-input.json"`
plus the `<` redirect back out both work.

**This is not the upstream bug.** The same `/tmp` assumption in tend's
own bundled scripts and skills is
[max-sixty/tend#1192](https://github.com/max-sixty/tend/issues/1192),
fixed by
[max-sixty/tend#1199](https://github.com/max-sixty/tend/pull/1199) and
merged 2026-09-09T08:23Z — about two hours after 0.2.5 tagged, so it
reaches this repo at the next release plus regeneration. That fix covers
bundled files only, and its own test asserts that root `/tmp` **stays**
read-only, so these repo-local recipes stay broken until they change
here.

One sentence was added next to the existing "don't `cargo install` in
the sandbox" note, so the next author editing these recipes knows why
the paths look like that.

<details><summary>Evidence and gate assessment</summary>

Found by the daily review-runs sweep, run
[34451797174](https://github.com/max-sixty/worktrunk/actions/runs/34451797174).

**Confidence** — structural, not stochastic: the write fails on the same
syscall every time, for every session, and it is reproduced above rather
than projected. Thirteen sessions in this window hit read-only `/tmp` on
other paths; the nightly
[34446121464](https://github.com/max-sixty/worktrunk/actions/runs/34446121464)
hit it eight times, including `tee: /tmp/tend-check.txt: Read-only file
system` and an `OSError: [Errno 30] Read-only file system` traceback out
of `nightly_workflow_update.py`. Neither of the two recipes changed here
was exercised in this window — no `tend-weekly` run, and no coverage
dispute — so the specific sites are unfired rather than observed
failing.

**Magnitude** — targeted fix: seven path substitutions and one sentence,
no new section and no new machinery.

**Cost** — waste-class, with a false-claim edge. A session whose codecov
recipe dies mid-investigation either improvises or, per
`running-in-ci`'s rule on unrun gates, has to report the gate as unrun;
the weekly statusline check is the one that could otherwise read as
clean. The remedy is a literal swap in existing lines, which clears the
bar a waste-class finding sets.

</details>

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
This commit is contained in:
Worktrunk Bot
2026-09-10 04:33:46 -07:00
committed by GitHub
parent f85230dc62
commit bdfee6a713
2 changed files with 10 additions and 8 deletions
+7 -5
View File
@@ -46,7 +46,9 @@ Don't try to `cargo install` them in the sandbox — past attempts at
source-compiling installs cascaded into bash-tool interrupts that blocked
even `pwd` and `echo`. Instead, query Codecov directly, following
`tests/CLAUDE.md`**Coverage Investigation** for the endpoints and their
traps.
traps. The sandbox also mounts root `/tmp` read-only, which is why the
scratch paths there and below go to `${TMPDIR:-/tmp}` — write new ones the
same way.
If the Codecov API markers aren't enough, download the `code-coverage-report`
artifact from the PR head's `coverage` workflow run — it contains a
@@ -58,8 +60,8 @@ REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
CI_RUN=$(gh api "repos/$REPO/commits/<sha>/check-runs" --jq '.check_runs[] | select(.name == "code-coverage") | .details_url | capture("runs/(?<id>[0-9]+)") | .id')
# List artifacts, then download the coverage one:
gh api "repos/$REPO/actions/runs/$CI_RUN/artifacts" --jq '.artifacts[] | {name, id}'
gh api "repos/$REPO/actions/artifacts/<id>/zip" > /tmp/coverage.zip
unzip -q /tmp/coverage.zip -d /tmp/coverage
gh api "repos/$REPO/actions/artifacts/<id>/zip" > "${TMPDIR:-/tmp}/coverage.zip"
unzip -q "${TMPDIR:-/tmp}/coverage.zip" -d "${TMPDIR:-/tmp}/coverage"
```
## Test Commands
@@ -349,7 +351,7 @@ jq -n --arg cwd "$PWD" '{
workspace: {current_dir: $cwd},
model: {display_name: "Opus"},
context_window: {used_percentage: 42.0}
}' > /tmp/statusline-input.json
}' > "${TMPDIR:-/tmp}/statusline-input.json"
# Debug build on purpose. `tend-weekly` installs no `wt` and restores no Rust
# cache, so `--release` means a cold optimized build of the whole dependency
@@ -357,7 +359,7 @@ jq -n --arg cwd "$PWD" '{
# check reads are profile-independent; only the timing columns, which this
# section doesn't triage, would be worth a release build.
cargo run -- -vv list statusline --format=claude-code \
< /tmp/statusline-input.json > /dev/null
< "${TMPDIR:-/tmp}/statusline-input.json" > /dev/null
cargo run -- config state logs profile --format=json | jq .cache
```
+3 -3
View File
@@ -61,17 +61,17 @@ For each uncovered function, either write a test (integration tests via `assert_
API=https://api.codecov.io/api/v2/github/max-sixty/repos/worktrunk
# Full SHAs throughout; an abbreviation 404s. `?pullid=N` compares the PR's
# *current* head, so name both SHAs to ask about an earlier commit.
curl -sL "$API/compare/?base=<base-sha>&head=<head-sha>" > /tmp/codecov.json
curl -sL "$API/compare/?base=<base-sha>&head=<head-sha>" > "${TMPDIR:-/tmp}/codecov.json"
# Patch coverage per file. `.name` is an object, and the files carrying patch
# lines are the ones with `has_diff`:
jq '.files[] | select(.has_diff) | {name: .name.head, patch: .totals.patch}' /tmp/codecov.json
jq '.files[] | select(.has_diff) | {name: .name.head, patch: .totals.patch}' "${TMPDIR:-/tmp}/codecov.json"
# The missed patch lines in one file. `.coverage.head` is a LineType enum
# (0=hit, 1=miss, 2=partial), and `.added` keeps context lines inside a hunk
# from reading as patch misses:
jq '.files[] | select(.name.head == "<path>") | .lines[]
| select(.is_diff and .added and .coverage.head == 1) | {line: .number.head, code: .value}' /tmp/codecov.json
| select(.is_diff and .added and .coverage.head == 1) | {line: .number.head, code: .value}' "${TMPDIR:-/tmp}/codecov.json"
# Whole-file line coverage at one commit. No trailing slash after the path —
# the route swallows it and answers 404 "coverage info not found":