Skip to content

fix: let a sandboxed session read the agent list, which silently truncates - #642

Merged
johanzander merged 2 commits into
mainfrom
fix/jobs-allowwrite
Aug 18, 2026
Merged

fix: let a sandboxed session read the agent list, which silently truncates#642
johanzander merged 2 commits into
mainfrom
fix/jobs-allowwrite

Conversation

@johanzander

Copy link
Copy Markdown
Owner

Problem

claude agents --json reads the session list from ~/.claude/jobs, which is not in the sandbox allow list. A sandboxed call does not fail — it silently truncates.

Measured in one session, seconds apart:

sandboxed:    1 agent
unsandboxed: 14 agents, 7 of them live sessions sitting in worktrees

All seven read as dead.

Why it matters

backlog-rhythm.sh keys resume_implementation off "worktree on disk, no live session". With a truncated listing it told the maintainer to re-enter a worktree that a live session was actively working — putting a second session on one branch, against commits the advice itself calls the only copy.

That happened tonight, on #619. implement-issue Step 0 reads the same list before touching a resumed branch.

An under-count rather than an error is the dangerous shape: nothing about a short list looks wrong, so the wrong answer is acted on with full confidence. It also defeated two layers of documentation — both backlog and implement-issue already carry a "run this unsandboxed" warning, and it still bit, because the warning only helps whoever remembers to read it.

Fix

One entry: ~/.claude/jobs in sandbox.filesystem.allowWrite, with the rationale added to CLAUDE.md's per-knob list, where every other non-default entry is already explained.

This makes the warnings unnecessary rather than merely repeated.

Test plan

  • ./scripts/quality-check.sh green
  • The 1-vs-14 measurement above, taken in this session

⚠️ Not verified in this session, and it cannot be

The sandbox captures its policy once at activation and ignores every later edit (CLAUDE.md, "The sandbox captures its policy ONCE"). So the numbers above come from the old policy, and re-measuring here would prove nothing — it would return the same 1 no matter what this diff says, which is exactly the trap that section documents.

Verifying needs a genuinely fresh session, then:

bash scripts/verify-sandbox.sh          # via the Bash tool, not a ! prefix
claude agents --json | jq length        # sandboxed; expect >1

Flagging this rather than claiming a green I cannot produce.

Scope

.claude/settings.json (one line) and CLAUDE.md (the rationale). No CHANGELOG entry: agent tooling, no user-visible effect.

Context

This is the one piece of #620 that survives. #635 superseded the rest of that PR by guarding pushes server-side with GitHub rulesets, which is a better answer than the pattern enumeration #620 was shrinking — but ~/.claude/jobs was unrelated to pushes and went with it.

🤖 Generated with Claude Code

…cates

`claude agents --json` reads the session list from `~/.claude/jobs`, which is
not in the sandbox allow list. A sandboxed call does not fail -- it TRUNCATES.

Measured in one session, seconds apart:

    sandboxed:    1 agent
    unsandboxed: 14 agents, 7 of them live sessions sitting in worktrees

All seven read as dead.

That is not cosmetic. `backlog-rhythm.sh` keys `resume_implementation` off
"worktree on disk, no live session", so with a truncated listing it told the
maintainer to re-enter a worktree that a live session was actively working --
a second session on one branch, against commits the advice itself calls the
only copy. It did exactly that for #619 tonight. `implement-issue` Step 0 reads
the same list before touching a resumed branch.

An UNDER-COUNT rather than an error is the dangerous shape: nothing about a
short list looks wrong, so the wrong answer gets acted on with full confidence.
Both skills already carry a "run it unsandboxed" warning; this makes the
warning unnecessary rather than merely repeated, and fixes the case where
nobody remembered to.

NOT VERIFIED IN THIS SESSION, and it cannot be: the sandbox captures its policy
once at activation and ignores every later edit, so the numbers above are from
the OLD policy and re-measuring here would prove nothing. Per CLAUDE.md this
needs a genuinely fresh session, then `bash scripts/verify-sandbox.sh`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012LExo6fcbup75vtc9NfoAR
@bess-agent

Copy link
Copy Markdown
Collaborator

@claude-bot review

Comment thread .claude/settings.json
"allowWrite": [
".",
"~/GitHub/bess-manager",
"~/.claude/jobs",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Root-cause check: the PR description says claude agents --json reads the session list from ~/.claude/jobs, but the fix adds the path to filesystem.allowWrite, not allowRead. This repo's own CLAUDE.md (unchanged by this PR, line ~510) documents that reads and writes are governed separately: "There is no allow-within-deny primitive for writes (reads have one, which is why allowRead differs)". Given the doc's own repeated lesson that sandbox-knob guesses are frequently wrong unless grounded in the actual error text (see the podman example a few lines below, where filesystem.allowRead was tried and disproved for a different symptom), and given the PR explicitly could not verify this fix in-session, is there a captured error message showing this is a write denial rather than a read denial? If not, allowRead looks like the more consistent knob for a stated read operation.

@bess-manager-claude-bot bess-manager-claude-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

Minimal, well-scoped, honestly-caveated fix: adds ~/.claude/jobs to sandbox.filesystem.allowWrite in .claude/settings.json, plus a matching rationale entry in CLAUDE.md's "Why each non-default knob is there" section. Two files, 18 lines, no scope creep — matches the PR's own stated scope.

No linked issue (closingIssuesReferences is empty) — the PR body itself documents the investigation (the 1-vs-14 agent-count measurement), so there's no external root-cause doc to cross-check against, but the in-PR evidence for there being a real problem is solid and specific.

Open question: is allowWrite the right knob for a read operation?

Left as an inline comment on .claude/settings.json:19, repeating here since it's the crux of the review:

The PR description says (its own words): "claude agents --json reads the session list from ~/.claude/jobs" — but the fix adds the path to filesystem.allowWrite, not allowRead. CLAUDE.md itself, unchanged by this PR (~line 510), already documents that reads and writes are governed by separate mechanisms in this sandbox config: "There is no allow-within-deny primitive for writes (reads have one, which is why allowRead differs)" — and a few lines below that, the podman entry is a worked example of exactly this failure mode: filesystem.allowRead was tried, and disproved, for a different symptom, precisely because reasoning about "what ought to be blocked" instead of reading the actual error picked the wrong knob.

This PR's own "Not verified in this session, and it cannot be" section is honest about not having confirmed the fix works — which is unavoidable given the sandbox's capture-once behavior, and matches this project's established process of merge-then-verify-in-a-fresh-session for sandbox.* changes (CLAUDE.md: "Verify with bash scripts/verify-sandbox.sh in a FRESH session after any change to sandbox.*"). So I'm not blocking on that part. But the read-vs-write mismatch is answerable now, from the PR's own description and the adjacent doc text, without needing a sandboxed session — and if allowRead turns out to be the actual fix needed, this PR would land, get documented as "fixed" (removing the very warnings it says become "unnecessary rather than merely repeated"), and the truncation bug would persist silently — which is the exact dangerous shape ("nothing about a short list looks wrong, so the wrong answer is acted on with full confidence") the PR itself calls out as the reason this matters.

Ask: before or immediately after merge, confirm with the actual captured error/behavior (not just "sandboxed count == 1") whether this is a write-permission denial or a read-permission denial on ~/.claude/jobs, the way the podman/keychain/TLS entries elsewhere in that same CLAUDE.md section were each pinned to a specific measured error message rather than an assumption.

Checklist

  • Matches root cause: no linked issue to check against; the PR's own evidence for the problem (1 vs 14 agents) is solid. The fix (allowWrite vs allowRead) is the open question above.
  • Test coverage: N/A by nature — sandbox behavior can't be tested in-session (correctly explained in the PR body), and ./scripts/quality-check.sh only asserts the Bash permission lists (deny/ask/allow), not sandbox.filesystem, so its "green" checkbox is real but doesn't bear on this specific fix's correctness.
  • Minimal / no scope creep: yes — one config line + matching doc rationale, consistent in style and placement with the surrounding "Why each non-default knob is there" entries.
  • Rule violations: none found in docs/agents/rules.md / .github/claude-bot.md terms (no new classes, no hardcoded entities, no exception-string matching, no missing camelCase conversion — none of these apply to a config/doc-only change). No CHANGELOG entry, consistent with recent precedent for agent-tooling-only PRs (#635, #637, #639 also skip it).
  • Scope statement (rules.md Debugging Protocol step 9): PR states scope explicitly (".claude/settings.json (one line) and CLAUDE.md (the rationale)") — a local, minimal change, correctly categorized.

Verdict: COMMENT — the allowWrite/allowRead question is worth resolving with real evidence before trusting this as done, but it's a low-blast-radius, easily-revertible config change from an author who was unusually careful to flag what wasn't verified, so it's not a blocking rule violation.

The Stage 4 review asked the right question: `claude agents --json` READS the
session list, so why does the fix touch `allowWrite` rather than `allowRead`?
It also named the precedent that makes the question worth asking -- the podman
entry a few lines below, where `filesystem.allowRead` was tried and disproved
by reasoning about what ought to be blocked instead of reading the error.

Answered by measurement rather than argument, in a sandboxed session:

    ls ~/.claude/jobs             ->  16 entries              # read:  ALLOWED
    touch ~/.claude/jobs/.probe   ->  Operation not permitted # write: DENIED

Reads were never blocked -- the read policy denies only `~/.claude/ide`. The
enumeration needs to write, and drops the entry rather than raising when it
cannot, which is what produces the truncation. So `allowWrite` is the correct
knob and `allowRead` would have changed nothing.

Recorded in the entry itself, in the same shape as the other knobs in that
section, each of which is pinned to a specific measured error rather than an
assumption. The next reader gets the answer without re-asking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012LExo6fcbup75vtc9NfoAR
@bess-agent

Copy link
Copy Markdown
Collaborator

Good question, and the precedent you cite is the right one to hold this against — the podman entry a few lines below is exactly a case of filesystem.allowRead being tried and disproved by reasoning about what ought to be blocked. So I answered it the same way that entry was answered: with two probes rather than an argument.

In a sandboxed session, against ~/.claude/jobs:

ls ~/.claude/jobs             ->  16 entries               # read:  ALLOWED
touch ~/.claude/jobs/.probe   ->  Operation not permitted  # write: DENIED

Reads were never blocked. The read policy in this sandbox denies exactly one path, ~/.claude/ide, and ~/.claude/jobs is not it — hence 16 entries listed. It is the write that is refused, and ~/.claude/jobs appears in the write policy's deny-within-allow list.

So the enumeration can see the job files and still under-reports, because it needs to write (a lock or heartbeat beside each job) and drops the entry rather than raising when it cannot. That is also the mechanism behind the shape I called dangerous in the description: a write failure surfacing as a shorter list instead of an error.

allowRead would have changed nothing here, and adding it would have been the wrong-knob mistake in the other direction.

Pushed as 9923b4bf — the measurement is now recorded in the CLAUDE.md entry itself, in the same shape as the other knobs in that section, so the next reader gets the answer without re-deriving it.

Your other two points stand and I have not tried to argue them away: quality-check.sh genuinely does not assert sandbox.filesystem, so its green says nothing about this fix; and the fix itself still cannot be verified in-session because the sandbox captures its policy once at activation. That verification is a fresh session plus scripts/verify-sandbox.sh, exactly as the description says.

@bess-agent

Copy link
Copy Markdown
Collaborator

@claude-bot review

@bess-manager-claude-bot bess-manager-claude-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

Minimal, well-evidenced infra fix: adds ~/.claude/jobs to sandbox.filesystem.allowWrite in .claude/settings.json (1 line) and documents the rationale in CLAUDE.md in the same style as every other non-default sandbox knob already documented there.

Root cause match: Yes. The PR traces the failure precisely — claude agents --json enumerates ~/.claude/jobs, and enumeration apparently needs to write there (confirmed with a direct probe: touch ~/.claude/jobs/.probeOperation not permitted, while ls succeeds). A sandboxed session missing that entry silently drops entries rather than erroring (1 agent vs 14, with 7 live sessions misreported as dead), which caused backlog-rhythm.sh to advise resuming a worktree a live session already held. Adding the directory to allowWrite is the direct fix for that missing permission — not a workaround routing around some other ordering/timing problem.

Test coverage: No automated test (this is a sandbox permission entry, not application code — consistent with how every other entry in that CLAUDE.md section is documented, by measured before/after behavior rather than a unit test). The PR includes concrete measured evidence (1-vs-14 agent count, and the second commit's read/write probe pair added in response to a prior review round asking "why allowWrite and not allowRead"). It's honest that the fix itself can't be re-verified in this session, since the sandbox captures its policy once at activation — that's consistent with what CLAUDE.md already documents about this class of change, and it correctly avoids claiming a green it can't produce.

Scope: Minimal and on-target — one settings entry plus the matching documentation. No scope creep. Correctly skips a CHANGELOG entry (agent tooling, no user-visible effect), consistent with the project's CHANGELOG conventions for user-facing changes only.

Rule violations: None found. This touches no Python/TypeScript application code, so the bulk of the architecture/error-handling/testing checklist items don't apply. JSON in .claude/settings.json is well-formed and the new entry follows the same tilde-path style as its neighbors (~/GitHub/bess-manager, ~/.npm, etc.).

Minor observation (non-blocking): Per rules.md Debugging Protocol step 9, the PR description doesn't explicitly label the fix as "local" — but given the change is a single config entry with a directly-matching root cause, the local-fix classification is self-evident here and doesn't need a separate callout.

No blockers. Approving.

@johanzander
johanzander marked this pull request as ready for review August 18, 2026 06:25
@johanzander
johanzander merged commit beaf562 into main Aug 18, 2026
8 checks passed
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.

2 participants