Interactive `claude` sessions are classified as background jobs post-2.1.139, causing bg-only guards to fire on user-foreground work
Summary
After the agent-view release (v2.1.139, May 11), every Claude Code session — including ones launched interactively by typing claude in a terminal — is set up with $CLAUDE_JOB_DIR, a daemon-managed state file, and a template: "bg" flag. As a result, instructions and guards that were intended for spawned/unattended background jobs now fire on user-foreground sessions too.
The most visible symptom is the worktree-isolation guard: every Edit/Write on a tracked file refuses with a "background session hasn't isolated its changes yet" error and forces the agent to do an EnterWorktree → commit → push → ExitWorktree → pull dance, even for a one-line edit in a session the user is actively typing in.
This is not a misconfiguration on the user's machine — it's the daemon's classification behavior. I reproduce it on a stock install on 2.1.143.
Repro
- macOS, fresh terminal. No
--bg, no agent-view, no shell wrappers. cd ~/some/git/repo && claude— open a normal foreground session.- From inside that session, ask the agent to make any single-file edit on a tracked file.
- Observe the Edit tool fail with:
> This background session hasn't isolated its changes yet. Call EnterWorktree first so edits land in a worktree instead of the shared checkout, then retry this edit using the worktree path. (To disable this guard for this repo, set "worktree": {"bgIsolation": "none"} in .claude/settings.json.)
- The agent's system prompt also includes the line "This session runs as a background job. The user may be chatting with you live or may have stepped away to check results later — respond naturally either way, and don't refer to yourself as 'a background agent.'" — which itself acknowledges the awkwardness.
Evidence the daemon is the cause
Process tree
A single claude invocation spawns a daemon + spare-agent pool:
44013 claude # foreground (terminal s000)
44581 claude daemon run --origin transient --spawned-by {...,"pid":44013}
44603 --bg-pty-host .../fb780c90.pty.sock 200 50 -- --bg-spare ...
44605 --bg-spare /tmp/cc-daemon-501/.../fb780c90.claim.sock
44608 --bg-pty-host .../90e4af4c.sock 162 36 -- --session-id 90e4af4c-... --agent claude
44611 --bg-pty-host .../30bad891.pty.sock 200 50 -- --bg-spare ...
44615 --session-id 90e4af4c-2ad8-4c0e-906e-7edbc3a3ad1e --agent claude
44616 --bg-spare /tmp/cc-daemon-501/.../30bad891.claim.sock
So:
- The foreground
claude(44013) auto-spawns a daemon (44581). - The daemon keeps a warm spare agent (44615) and bg-pty hosts ready, presumably so
←(agent view) feels instant. - Everything — including the user's interactive session — gets registered as an "agent" with a job dir.
Job-dir contents for the foreground session
$ env | grep CLAUDE_JOB_DIR
CLAUDE_JOB_DIR=/Users/smabe/.claude/jobs/ec39b0b1
$ cat $CLAUDE_JOB_DIR/state.json | head -20
{
"state": "blocked",
"tempo": "active",
...
"template": "bg",
"respawnFlags": ["--effort", "high", "--permission-mode", "auto"],
"name": "push-code-changes",
"nameSource": "auto",
...
"cliVersion": "2.1.143",
"cwd": "/Users/smabe/projects/HealthData",
...
}
template: "bg" is set on the user-foreground session. The daemon also auto-named the session ("push-code-changes") from the agent's last bash output, the way agent-view names spawned background jobs.
Why this is a bug, not a feature
Several harness-level behaviors were written for the old model where $CLAUDE_JOB_DIR implied "unattended bg work that may collide with the user's working copy and other parallel jobs":
- Worktree-isolation guard — refuses Edit/Write on
tracked filesuntilEnterWorktree. Sensible for an unattended job; pure friction for a user typing into the same session. - System-prompt "Background Session" block — tells the agent the user "may have stepped away." Wrong for the user-foreground case (and the prompt now has to apologize for itself with "respond naturally either way").
- Auto-naming — naming the session from agent output (
"push-code-changes") is great for unattended jobs the user finds later in agent-view. Less useful, mildly confusing, for an interactive terminal session.
Discrimination signal exists but isn't being used. The daemon already has:
--origin transientvs other origins--spawned-by {label, cwd, pid}for spawned jobs--bg/--bg-sparefor actual bg pool entries vs the user's TTY-attached session- A TTY-attached child process visible to the daemon
Any of those could classify "the user is talking to this session live" vs "this session was spawned to run unattended."
Suggested fix
Distinguish the two cases at the daemon layer and propagate to:
- Set
template: "foreground"(or omittemplate) on user-typed sessions; reservetemplate: "bg"for sessions spawned viaclaude --bg, agent-view "new background session,"Agenttool subagents,TaskAPI spawns, etc. - The worktree-isolation guard should fire only on
template: "bg", not on the presence of$CLAUDE_JOB_DIR. - The "Background Session" block in the system prompt should only be inserted for
template: "bg". - Auto-naming from agent output should only happen for
template: "bg".
The bgIsolation: "none" setting documented in the guard message is a workaround that disables protection wholesale, which is the wrong knob — users running real parallel bg work in the same repo want the protection on; they just don't want it firing on their interactive session.
Environment
claude --version→2.1.143 (Claude Code)- macOS 25.5.0 (darwin), zsh, Ghostty terminal
- Plain
claudeinvocation in a project directory, no flags, no shell wrappers
Related
- #59702 —
EnterWorktree/ExitWorktreecwd-pinning bug (filed today by another user). Compounds with this one: the forced worktree dance is the trigger surface for #59702, so fixing this issue would reduce exposure to that one too. - #59846 — Naming feedback: "agent view" conflates with the existing "agent" concept. Separate concern, same underlying conflation: "agent" and "session" are no longer cleanly distinguished post-2.1.139, and harness behaviors keyed off "is this a session?" assumptions break in surprising places.
8 Comments
This is a clear write-up of an architecturally interesting bug, and the root cause you identify — "
$CLAUDE_JOB_DIRis being used as a proxy fortemplate == 'bg'" — is worth naming as its own concern, because the same conflation is going to keep producing surprises across other subsystems that branch on session classification.A few observations from running supervisor-shaped processes that orchestrate Claude Code daemons (separate top-level process, owns lifecycle of a pool of
claudeinvocations, distinguishes user-interactive vs. unattended children):The "is this background" question has at least four orthogonal axes, and v2.1.139 collapsed them into one signal (
$CLAUDE_JOB_DIR != ""). Concretely:| Axis | Question | Honest signal |
|---|---|---|
| TTY-attachment | Is a human typing at this process right now? |
isatty(stdin)of the agent process || Spawn origin | Did a user invoke this, or did another agent/the daemon spawn it? |
--spawned-by/--origin transientvs--bg|| Persistence | Will this process outlive the user's terminal session? |
--bgflag / process-group detachment || Worktree isolation policy | Are concurrent edits expected against the same checkout? | repo-level config / explicit policy |
The worktree-isolation guard is keyed off axis 4, but the daemon is computing axis 4 from axis 3 via
$CLAUDE_JOB_DIR, which is itself a proxy for whether the daemon has registered the session (which now happens on every invocation since 2.1.139). All four axes used to align by accident — pre-2.1.139, only spawned bg jobs got a job dir, so the four-way agreement was implicit. The agent-view release broke that alignment by making the warm-spare pool real, and the system-prompt apology ("respond naturally either way") is the artifact of that decoupling not being fully threaded through downstream consumers.The proposed fix (
template: "foreground"vstemplate: "bg") is correct but is one of two complementary moves. The other is: every consumer of "is this background?" should branch on the specific axis it actually cares about, not ontemplate. The system-prompt block is genuinely about TTY-attachment (axis 1) — "the user may have stepped away." Auto-naming is about persistence (axis 3) — agent-view needs a label to surface in a list later. The worktree-isolation guard is about edit-collision policy (axis 4). Routing all three throughtemplateworks, but the next subsystem that wants to branch on "is the user typing" will reach for$CLAUDE_JOB_DIRagain iftemplateis the only public signal, and you'll get the same conflation in a different surface six months from now. Exposing the four signals separately (or at minimum:tty_attached,spawned_by_user,persistent, and lettingtemplatebe a derived convenience) is more defensible.On the
bgIsolation: "none"workaround being the wrong knob: agree completely, and the reason matters. The user wants the guard for actual bg work in the same repo — operators running real fleets (1 user + N supervised agents in distinct worktrees) need that collision protection. The workaround disables protection wholesale; the right fix only disables it on TTY-attached sessions. This is identical in shape to thepermissions: session-scopedissue raised in #54898 — once permissions/guards/templating are session-scoped but the underlying axes are agent-scoped, every per-agent policy decision has to be hand-routed.One small empirical addition you might be able to verify quickly: is the auto-name (
"push-code-changes") being set from agent output after the agent's first bash call, or pre-emptively fromcwdat spawn time? On the supervised-process side, it appears post-first-bash — which means the auto-naming pipeline is consuming bash output on every session including foreground ones, presumably to feed the agent-view sidebar. If so, the relevant fix isn't just suppressing the write of the name on foreground sessions, but suppressing the read of bash output by the daemon's naming subsystem when the session is TTY-attached. That's a different code path than the four behaviors you list.The TTY check (axis 1) is the cheapest, most legible signal of "user is actively here" — and it's the one the OS already gives you for free, untouched by daemon registration timing.
Hey thanks for the reply. I noticed that the auto name happened after the first bash call
Also running in to issues working in the main chat session
<img width="1628" height="669" alt="Image" src="https://github.com/user-attachments/assets/19ac064b-df59-49cd-bcdc-c531ea27d7ad" />
Adding a fresh repro + two new subcases from a 4-hour autonomous-planning session today (v2.1.143, macOS, stock install). Context: I was working in a bg job orchestrated by clu, my personal plan-orchestrator. Bg job, but the operator was actively chatting with me throughout — different from the issue body's foreground case, same friction.
Hit
bgIsolation5 times for what should have been single-shot edits:plans/— EnterWorktree → write → commit → ExitWorktree → ff-merge → push → cleanup. ×3.Each round-trip cost ~5 min wall time, one prompt-cache miss, and forced operator-visible merge commits for what could have been linear edits. Net session cost: ~15 min friction + one entire 5-min cache window invalidated mid-flow.
New subcase the issue doesn't enumerate: bg session where the operator is actively present. The system prompt's "respond naturally either way" line covers this rhetorically but the guard treats all bg sessions as if the operator is away. Per @kcarriedo's four-axis breakdown — "is the operator present in this conversation right now" is arguably a fifth axis (or a refinement of TTY-attachment for the bg case). Today, even when the operator types into a bg job's chat continuously, the guard fires.
New failure mode: edits blocked mid-merge. During a
git merge --no-ff <branch>that produced conflicts in two files, I couldn't use Edit to resolve the conflict hunks (guard fired). EnterWorktree mid-merge isn't an option — the merge state is onmain, not transferable. I escaped by writing a python heredoc that rewrote each conflict block viaPath.read_text().splitlines() + slicing. Worked, but the natural tool path was Edit and the workaround added ~5 min with zero safety upside (the merge was already creating commits on main; the guard's "you might clobber the user's checkout" premise is moot oncegit mergeis in flight).A carve-out for
state == MERGEwould close this specific cliff cheaply. Longer-term fix is per @kcarriedo's analysis — route each guard's decision through the axis it actually cares about. For this specific guard (axis 4 = edit-collision policy), "is there an active merge in progress?" is a hard "no other agent should be touching this checkout anyway" signal that overrides the worktree-isolation policy without needing the full axis cleanup.Can confirm that adding below line to
.claude/settings.jsoneliminates the problem.Hit by this on a Linux ARM64 server (Ubuntu 24.04 aarch64, Hetzner cax41, 16 cores / 32 GB RAM), Claude Code v2.1.145.
Repro / impact:
The orphan
--bg-spareand--bg-pty-hostdaemons survive the parent claude session exit and accumulate on long-lived hosts. After ~30 hours of normal use (multipleclaude --resume/--dangerously-skip-permissionssessions in tmux), I had two daemon processes hammering the disk continuously:Combined ~1.27 GB/s sustained disk read from the two orphans alone. With other Claude sessions still running (
--resume <uuid>workers spawned by an autonomous orchestrator session), aggregate hit ~2 GB/s and load average climbed to 105 on a 16-core box. The Hetzner Cloud metrics graph shows the sustained 1500% CPU + 2 GBps read pattern for hours.Process tree confirms the parent claude TUI was long gone but the daemons kept running with
--origin transientstyle cmdlines.What broke as a side effect:
ugrepworkers (spawned by Claude for file indexing) piled up to ~266 MB/s read each, presumably waiting on the same daemons.Workaround that worked:
Load went from 105 → 10 in ~30 seconds, disk read collapsed to baseline immediately. No data loss, all open
claude --resumesessions in tmux were unaffected (the orphan daemons had no parent TTY left).Asks:
$CLAUDE_JOB_DIRis orphaned for >N seconds).~/.claude/projects/<encoded-cwd>/and/or workspaceugrepindexing on every event — could that loop be bounded / cached?--no-daemon/CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1that is actually respected for interactive sessions would unblock this.Downgrading to 2.1.104 (the last version before the agent-view bg-pool change) is my current plan.
MacOS, Cross-session filesystem corruption from EnterWorktree/ExitWorktree, 2.1.150
Adding a huge secondary issue that I am also facing related to this. When one session calls
EnterWorktreeorExitWorktree, it corrupts filesystem permissions for ALL other concurrent sessions across the entire repo — not just the session that invoked it. I don't know what causes this, but it happened twice back to back after upgrading to 2.1.1.50Repro:
EnterWorktreeby the bg isolation guardExitWorktreeto return to mainEPERM: operation not permittedon Read, Write, and Edit — even on files in completely separate worktreesImpact:
Cannot be blocked:
EnterWorktreeandExitWorktreeare not in the deny-list schema — the regex pattern forpermissions.denydoesn't include them as valid tool namesAgent,Bash,Edit,Read,Write)bgIsolation: "none"is supposed to prevent the guard from firing, but the guard fires anyway (the core issue in this thread)This is a data-safety issue. The worktree lifecycle is modifying filesystem permissions on the main checkout and other worktrees that it has no business touching. This is incredibly annoying and dangerous. This is causing file permission corruption.
I don't know if this is also an issue with some new claude daemon and updating. I updated, but I'm not sure how to restart the DAEMON itself?
Fixed — interactive sessions are no longer misclassified as background jobs. Please reopen if you still see bg-only guards firing on foreground work in a current version.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.