Repository navigation
skills(running-in-ci): say /tmp is read-only, not just where $TMPDIR points - #1244
Conversation
tend-agent
left a comment
There was a problem hiding this comment.
The claim as written is false for the session's own working directory, which weakens the paragraph it is meant to strengthen. The sandbox's write allowlist is allowWrite: [agentWorkspace, agentHome, autoMemory] (shared/steps/sandbox_runtime.mjs), and prepare_agent_workspace.py clones TEND_AGENT_WORKSPACE into what its module docstring calls a "dedicated /tmp container" — so the checkout a session edits all run long is itself a /tmp/... path, and it writes fine while its parent container does not. Verified from inside this session: a write to the checkout succeeds, one to the workspace root takes Read-only file system. A session reading "the sandbox mounts /tmp read-only, so a hand-written /tmp/... path fails" hits a counterexample on its first file edit, and the sentence stops being something it can act on.
The suggestion keeps both of the change's points — the token the model habitually reaches for, and the misleading mode — while naming the writable set accurately. It still contains /tmp, so it still depends on the guard exemption in 456d933.
The checkout the session works in is itself under /tmp: prepare_agent_workspace.py clones it into a dedicated /tmp container, and sandbox_runtime.mjs allows writes there and in the agent home. "The sandbox mounts /tmp read-only" is contradicted by the session's first file edit, so state what is writable and that the container around the checkout is not.
SRT read-only-binds the whole root and rw-binds only what tend lists in `allowWrite`, which never included `/tmp`. Nothing protects on that — it was simply never listed — so every tool that hard-codes a path under `/tmp` has needed a workaround of its own: a `TMPDIR` override (#1199), a zsh `TMPPREFIX` override (#1211), skill text warning sessions off it (#1244), and now NuGet's build mutex (#1312), which no environment variable moves and which a consumer is working around per-repo. This fixes the layout instead, so the family ends. `/tmp` is writable inside the sandbox now. The two runner-owned containers — the per-run SRT runtime and the disposable event checkout — move to `/var/tmp`, which is 1777 like `/tmp` but outside the write policy, so from inside they stay exactly as unwritable as before. The Claude auto-memory working copy moves with them, since the dispose step runs before the save that reads it. After the reap, that step also removes the top-level `/tmp` entries the sandbox uid owns: `/tmp` is sticky, so ownership is the whole test, and every runner-owned entry beside them stays. Reading a directory the whole runner shares races it, so an entry that disappears between the listing and the stat is treated as the gone thing it is rather than failing an `always()` step and filing a `tend-outage` issue over someone else's temp file. Ownership separates the two at the top level alone, which the security model now says. Deleted with the constraint that produced them: the `TMPPREFIX` override and its reserved `sandbox_env` key, and the repo-wide test banning bare `/tmp` paths in shipped instructions. `run-tend`'s temp-files section is down to the one fact a session still needs — `$TMPDIR` is `/home/tend-sandbox/tmp`. `TMPDIR` and `AGENT_TMP_DIR` themselves stay: the supervisor reads the step summary from that directory, and the shipped skills name `$TMPDIR` throughout. So does the `CLAUDE_CODE_TMPDIR` override, for a reason the comment now states — with filesystem policy on, SRT overwrites `TMPDIR` in the child environment with its own `/tmp/claude` default unless handed a path. The check that matters is `test-sandbox`, which drives the real boundary on a hosted Ubuntu runner. Its probe for an unwritable path now aims at `/var/tmp` rather than at the 755 container directories, where a non-root uid would be refused on file permissions alone and the assertion would hold whether or not the boundary was still there. A runner-owned file planted before the lifecycle covers the other half: the sandbox must fail to unlink it — the sticky bit is all that protects runner state in the shared directory while the agent runs — and a new step then runs the real dispose against the real filesystem, where sandbox-written scratch goes and that file stays. Closes #1312 > _This was written by Claude Code on behalf of max-sixty_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
running-in-ci's Temporary Files section names$TMPDIRas the writable scratch directory but never says/tmpis not one, so a session that writes its own scratch path reaches for the habitual/tmp/...and takesRead-only file system. The checkout it works in is itself under/tmp(prepare_agent_workspace.pyclones into a dedicated container there), so the constraint has to name the writable set rather than the/tmpprefix. Nothing in the environment warns it first —/tmpis modedrwxrwxrwt, and only the mount is read-only, because SRT's write allowlist is the agent workspace, the agent home, and the auto-memory directory (sandbox_runtime.mjs). #1192 moved the bundled scripts and recipes off/tmpand #1211 fixedTMPPREFIX; neither covers a path the model writes itself.The failure is usually one wasted turn, but #1242 records a nightly where the dead write was the backup step of a back-up → revert → test → restore sequence run as one compound command with no
set -e: the backup never existed, the restore silently failed, and the reverted file survived in the worktree.Solution
One clause on the existing sentence, so the constraint arrives with the fact it qualifies: writes land in
$TMPDIRand the checkout, and the/tmpcontainer around the checkout does not take them. Plus a sentence naming the compound-command shape that turns the failure from a wasted turn into a bad push.test_bundled_runner_guidance_has_no_unscoped_tmp_pathsbans a bare/tmpanywhere underplugins/tend-ci-runner/, which is why no bundled file could state the constraint. The guard now scans per line and exempts a line that says/tmpis read-only — that line is the rule, not an instance of the failure. Every other mention still fails the test; I confirmed by probing it with a scratch file carrying/tmp/foo.md.Testing
Prose change; no test covers the skill text. Verified the claim from inside this session's sandbox:
uv run pytestpasses apart from the twotest_refresh_consumers.pycases that need DNS, which the sandbox has none of.pre-commit run --all-filespasses, including the threerepo: localguards.Closes #1242 — automated triage