Other

/tmp-block

Fail-CLOSED PreToolUse guard denying durable-content writes to temp paths (/tmp, /private/tmp, /var/folders, $TMPDIR) across Write/Edit and Bash writes, and enforcing the ratified <repo>/.worktrees/<name> worktree location. Triggers: tmp guard, TMP-BLOCK deny, WORKTREE-CONVENTION deny, bypass ledger. NOT for storage policy.

$ golems-cli skills install tmp-block

Updated yesterday

Hook-carried skill. The enforcing artifact is hooks/tmp-block-pretooluse.py (PreToolUse on Write|Edit|NotebookEdit|Bash); this page is its contract and audit guide.

Why it exists (S04, weave 2026-06-07 Phase-2 Fix-3)

A worker ran Write(/tmp/orqi-tts-answer-msg.md); the era's /tmp guard half-fired (validated then allowed) — a hook validation error let the durable write through, and Etan caught it live ("Wait, why are we writing those things in temp?"). Class evidence: ≥24 /tmp anti-pattern findings, 4 proven costs in 24h (reboot-wiped leak worksheet, a harness fix stranded in a prunable /tmp worktree, Etan's own terminal burned by a /tmp-worktree-held branch). Adversary verdict: KEEP, with the deny extended to the canonicalized path-CLASS and to Bash writes, "else it's one directory away from useless."

The contract — two-valued (Etan, by voice, 2026-08-17)

"none of y'all would be able to write to temp, but also not ask me so we don't get agent stuck."

Provably outside the temp class -> ALLOW. Everything else -> DENY, with a reason the agent can act on. This hook NEVER emits a PreToolUse prompt.

A prompt suspends the pane until a human answers it, and a headless Codex or Cursor worker has no human in its pane at all — 2026-08-14/17 lost hours to ~/Documents probes stranding panes overnight. A deny comes back as a readable error the agent reroutes around by itself, so the residual failure mode is the recoverable one.

SurfaceBehavior
Write/Edit/NotebookEdit into the classDENY + redirect to the repo / docs.local/
Bash output redirect (>, >>, &>), incl. heredoc+redirectDENY
Bash tee / tee -a into the classDENY
git worktree add into the classDENY + redirect to <repo>/.worktrees/
git worktree add outside any .worktrees/ parentDENY + the exact fixed command (Rule 2)
git worktree add whose target can't be resolvedDENY, naming the exact resolution failure (golems#676's requirement, minus the prompt)
Any target the hook cannot read staticallyDENY — rewrite it with a literal path, or a variable whose value has a literal head
Reads/deletes (ls, cat, grep, rm, worktree list)NEVER denied
Any hook/validation error (incl. unwritable ledger)DENY — fail CLOSED (the S04 half-fire class)
CLAUDE_WORKERdoes NOT exempt (S04's violator was a worker)
The harness session scratchpadALLOW — the one sanctioned temp location (below)

Path-CLASS (canonicalized via realpath): /tmp, /private/tmp, /var/folders, /private/var/folders, the live $TMPDIR value, and literal $TMPDIR tokens in Bash commands. The macOS /tmp → /private/tmp symlink is not a route-around.

The one exception — the harness session scratchpad (2026-08-17)

Claude Code's own system prompt hands every session a scratchpad and instructs it, verbatim: "IMPORTANT: Always use this scratchpad directory for temporary files instead of /tmp or other system temp directories". The path it supplies is inside the class this guard denies:

/private/tmp/claude-<uid>/<repo-slug>/<session-uuid>/scratchpad/…

So the harness said "put temp files here" and the guard said no. Observed live 2026-08-17: brainlayerClaude took two consecutive denials arming a monitor, and skillcreatorClaude hit the same wall writing a test fixture the same day. Every agent walks into it, because they are following instructions correctly — the same failure shape as the "always notify on commits" flood.

Etan's ruling, 2026-08-17: allowlist the harness session scratchpad, keep denying every other temp path. It is session-scoped and nothing durable belongs there, so allowing it costs nothing Rule 1 was protecting.

The exception is structural and narrow — never a hardcoded uid or session id, and not a widening of the temp path-class. The whole claude-<uid>/<slug>/<session-uuid>/scratchpad chain must sit directly under a temp root, and every component is matched exactly: claude-<digits> (not any directory), a slug, a session UUID, and scratchpad (not scratchpad-evil or myscratchpad). The #727 review found both of those relaxations surviving the suite, so all three are now pinned by deny-side tests that redden the mutation.

It applies to every Rule 1 write surface — Write/Edit, redirects, appends, tee, heredocs — not to redirects only. Still denied: /private/tmp/scratchpad/x.txt (no session chain), /private/tmp/claude-501/x.txt (no scratchpad component), every $(mktemp) form (a bare temp path with no chain at all), /tmp/foo.txt.

.. traversal is defended by the callers, not by the shape match. The path is canonicalized before the shape is consulted: in_temp_class normalizes each candidate (normpath and realpath) and only then asks whether it is scratchpad-shaped, so …/scratchpad/../../../../leak.txt arrives as /private/tmp/leak.txt and denies; _literal_prefix_class rejects a literal .. component outright before building a probe. The normalization inside the shape check itself is defense-in-depth for a direct caller — the #727 review's mutation M5 removed it and opened no hole.

scratchpad is its own prefix-class verdict, decided independently of the repo/outside/temp proofs below.

Rule 2 — the worktree location convention (2026-08-09)

Ratified by Etan by voice, 2026-08-09: the fleet worktree location is the in-repo <repo>/.worktrees/<name>.

The gap this closes is Etan's own catch, verbatim: "I thought the guard's job was to guard it, so it actually goes the right way." Rule 1's deny message advised <repo>/.worktrees/ — but nothing enforced it, so git worktree add ~/Gits/foo.wt/bar (durable, not temp) sailed straight through and 18 sibling *.wt directories accumulated under ~/Gits/.

Resolved targetDecision
has a .worktrees ancestor componentallow
resolves anywhere elseDENY, naming the convention + git -C <repo> worktree add <repo>/.worktrees/<name>
cannot be resolved ($UNSET, $(…), relative after cd/git -C)DENY, naming the resolution failure

Rule 2 silently allows a target whose literal or already-resolvable, repo-anchored prefix proves .worktrees/ containment before an unresolved variable suffix, such as git worktree add .worktrees/$wt or $HOME/Gits/golems/.worktrees/$wt. An unresolvable variable prefix still asks; a literal .. escape still disqualifies the proof; and lexical plus realpath containment checks still reject a .worktrees symlink that leaves the repo.

golems#676 applied. git-guardian judged the unexpanded literal — it blocked a legitimate cleanup of a mktemp -d variable, then blocked the gh issue create that filed the bug because the title carried the pattern as prose. So Rule 2: expands ~/$NAME from the live environment and judges the resolved path (lexical and realpath, so a .worktrees symlink pointing at a sibling is not a route-around); fires only in command position (quoted text and heredoc bodies run nothing); and names the exact resolution failure instead of blocking blind when the target is unknowable. #676's requirement was the actionable reason, not the prompt — the prompt was dropped on 2026-08-17 and the reason kept.

Migration-window hatch, mirroring Rule 1: WEAVE_ALLOW_WT_MIGRATION=1 — allowed AND logged. It is location-scoped only; it never unlocks the temp class (that still needs WEAVE_ALLOW_TMP).

This is the backstop, not the only fix. It sees git worktree add issued as a Bash tool call. Generators that create worktrees in-process never reach it — cmuxlayer/src/worktree.ts:240 builds join(homeGitsDir, ${repo}.wt , spec.name), which is where the 18 dirs actually came from, and is fixed in the cmuxlayer/golems lanes. Likewise a worktree add nested in a quoted payload (bash -c "git worktree add …", an agent-spawn prompt string) is one token to this parser and is not caught.