Skip to content

skills(running-in-ci): say /tmp is read-only, not just where $TMPDIR points - #1244

Merged
max-sixty merged 3 commits into
mainfrom
fix/issue-1242
Sep 14, 2026
Merged

max-sixty merged 3 commits into
mainfrom
fix/issue-1242

Conversation

@tend-agent

@tend-agent tend-agent commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

running-in-ci's Temporary Files section names $TMPDIR as the writable scratch directory but never says /tmp is not one, so a session that writes its own scratch path reaches for the habitual /tmp/... and takes Read-only file system. The checkout it works in is itself under /tmp (prepare_agent_workspace.py clones into a dedicated container there), so the constraint has to name the writable set rather than the /tmp prefix. Nothing in the environment warns it first — /tmp is mode drwxrwxrwt, 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 /tmp and #1211 fixed TMPPREFIX; 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 $TMPDIR and the checkout, and the /tmp container 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_paths bans a bare /tmp anywhere under plugins/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 /tmp is 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:

$ ls -ld /tmp
drwxrwxrwt 18 nobody nogroup 4096 Sep 14 08:12 /tmp
$ touch /tmp/probe-tend-1242
touch: cannot touch '/tmp/probe-tend-1242': Read-only file system

uv run pytest passes apart from the two test_refresh_consumers.py cases that need DNS, which the sandbox has none of. pre-commit run --all-files passes, including the three repo: local guards.

Closes #1242 — automated triage

@tend-agent tend-agent left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread plugins/tend-ci-runner/skills/running-in-ci/SKILL.md Outdated
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.
@max-sixty
max-sixty merged commit 93a06c6 into main Sep 14, 2026
11 checks passed
@max-sixty
max-sixty deleted the fix/issue-1242 branch September 14, 2026 16:53
max-sixty added a commit that referenced this pull request Sep 19, 2026
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>
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.

running-in-ci: Temporary Files says where the writable scratch dir is, never that /tmp is read-only

2 participants