>_

Run Parallel Claude Code Sessions From One Terminal [2026]

Robin||20 min
claude-codeorchestrationagentsbackground-agents
Parallel Claude Code sessions driven from one terminal - claude --bg background agents, agent view, cross-session messaging, SendMessage, ListAgents, claude agents --json --all, state field, worker orchestration, Evolving Lite, primeline-ai/evolving-lite

Running parallel Claude Code sessions usually means a second terminal window, then a third, then a tab you forgot was waiting on a permission prompt twenty minutes ago. The work is not the hard part. Knowing which one needs you is.

In March I built a tmux layer to solve this, and I still maintain the repo. Most of it is now unnecessary. Claude Code ships the whole mechanism natively, and the piece that used to be missing, the worker reporting back to you, has been there since v2.1.224. Everything below runs on a stock install. The system I hang off it lives in primeline-ai/evolving-lite.

3commands, no extra tooling
3-9sthe state field lagged the finished work by
2JSON fields that vanish when a worker exits
2.1.233version everything here was measured on

Why parallel Claude Code sessions get lost

Parallel Claude Code sessions fail on observation, not on execution. Spawning a second session has never been hard. The trouble is that a terminal window is a terrible status display: it shows you the last thing that scrolled past, and nothing about whether the session is working, waiting on you, or finished ten minutes ago.

My tmux orchestration layer existed because of a fourth problem on top of that: in early 2026 subagents had a hard turn budget, no hooks, and no MCP access, so a real worker had to be a full session. That gap closed. If you want the history, I kept it honest in the tmux orchestration post, which now opens by saying most of what it describes was absorbed.

What replaced it is three commands and one habit.

How do you start a second Claude Code session in the background?

Run claude --bg with a prompt. It starts a full Claude Code session, detaches it from your terminal, prints the session ID, and returns immediately. Add --name so the session shows up under a label you chose rather than one Claude Code generated.

bash
cd ~/projects/api
claude --bg --name api-worker "investigate the flaky auth middleware test"

That prints the handle you use for everything else. This is the real output from blogdemo, one of several throwaway sessions behind the measurements below:

code
Starting background service…
backgrounded · 497a8868 · blogdemo
  claude agents             list sessions
  claude attach 497a8868    open in this terminal
  claude logs 497a8868      show recent output
  claude stop 497a8868      stop this session

This is a full conversation, not a subagent. Do not reach for the usual distinctions here: subagents also get their own context window, their own hooks and their own MCP servers, and I have said so elsewhere on this site. What a background session has instead is that it survives you closing the terminal, and that it stays addressable across rounds rather than returning one report and ending. claude agents opens agent view, one screen showing every background session grouped by state.

claude agents --json is not the same view in text form, and the difference matters later. Agent view lists background sessions. The JSON form prints every live session including your interactive terminals, and --all adds completed background sessions on top. It also does not need an interactive terminal, so it works inside a script.

Subagent or background session?

Reach for a subagent when you want a bounded piece of work to come back as a report inside your current conversation. Reach for a background session when the work is long, needs to keep going while you close the laptop, or needs to take follow-up instructions over several rounds. I use subagents constantly for scored delegation and background sessions rarely, for exactly two cases: a long build I do not want to sit through, and a genuinely independent second stream.

How does a background Claude Code session report back?

You ask your own session to talk to it, in plain English. Cross-session messaging gives Claude two tools, ListAgents to see which sessions it can reach and SendMessage to deliver text to one by name. You never call either one yourself. You type something like this in your main session:

text
Ask api-worker whether the flaky test reproduces, and tell me what it says

From v2.1.232 you can name the target with an @ mention and a typeahead picker, the same way you mention a subagent. /list-agents (also /peers) prints what the current session can reach.

Expect one wrinkle on your first send. A bare name delivers when exactly one live session answers to it, but when several share a name, or when Claude Code could not check everywhere your sessions run, it falls back to a short identifier and asks you to re-send with that. Connecting to Remote Control makes the second case common, because there are then more places to check. It is not a failure, it is one extra round trip. Starting a conversation with a session on another machine needs v2.1.225 or later. Before that a session could only reply to a message that arrived from one.

What actually crosses the wire

A message is plain text one Claude writes to another. Never files, never conversation history. Same-machine delivery goes over a per-session socket and never touches Anthropic servers. Reaching a session on another of your machines does go through Anthropic, arriving over that machine's Remote Control connection. If your sending session is not on Remote Control the message still goes through, but with no reply address, so nothing can answer it. Either way, once delivered the message counts toward usage like a prompt you typed there.

This is the leg that used to be missing, and its absence is why orchestration setups grew so elaborate. It is now the least interesting part of the problem. The interesting part is knowing when the worker is done, and that is where I got it wrong.

The state field lagged the finished work by six seconds

I gave a background session one trivial job: append a line to a file. Then I polled claude agents --json --all every three seconds and watched the state field next to the file at the same time.

code
t=3s   state=working   file=(no file)
t=6s   state=working   file=ROUND-1
t=9s   state=working   file=ROUND-1
t=12s  state=done      file=ROUND-1

The work was finished and on disk at the six second poll, and the state field did not flip until the twelve second one. Be careful reading a precise number off that: the file could have appeared any time after the three second poll and the flip any time after nine, so the true lag is somewhere between three and nine seconds. Three second polling cannot narrow it further. On a faster round in an earlier run, polling every two seconds, the field never left done at all: the entire turn started and finished between two polls.

The rule this earns

"The state did not change" is not "nothing happened". For a dispatch whose duration you do not control, the completion evidence is the artifact the worker produces, never the status field. Ask for a result file in the launch prompt, then watch the file.

So the launch prompt should end with something like write only the result to /tmp/api-worker.out. That one line turns a guess into a check. This is the same trap I wrote up in the hooks that fired 574 times and blocked nothing: an instrument that reports faithfully about its own machinery and tells you nothing about the thing you actually care about.

This lives in primeline-ai/evolving-lite - the self-evolving Claude Code plugin. Free, MIT, no build step.

A session that says done still takes work

A finished background session is not a closed one. It keeps its conversation and its context, and a peer message starts a fresh turn in it.

I sent a second instruction into the session that had already reached state: done. Two seconds later the file held both lines:

code
ROUND-1
ROUND-2

That is the part worth internalising. done describes the last turn, not the session. You can hand a worker round two, round three and round four without relaunching it, and it still remembers round one. claude stop is for a session that is genuinely still running. Run it against a finished one and it prints the same cheerful confirmation while the row does not change, so that string is an acknowledgement of your command, not a report about the session.

Why does my filter miss finished background sessions?

Because two of the fields you would filter on disappear when the worker's process exits, and because the default listing drops those sessions entirely. claude agents --json does not emit one uniform row. Which fields a row carries depends on what kind of session it is and whether its process is still alive.

FieldPresent
cwd, kind, startedAtalways
idbackground sessions, and it is what attach, logs and stop take
statebackground sessions: working, blocked, done, failed, stopped
pid, statusonly while the process is alive
waitingForonly when status is waiting
sessionId, namewhen set

I got this wrong on the first pass, in exactly the way this post is about. I took one snapshot, saw 16 background rows all carrying state and none carrying status, and wrote down that the two kinds have separate schemas. They do not. Every one of those 16 processes had already exited. When I launched a fresh worker and read the row while it was still running, it carried id, state, pid and status at once: state: working and status: busy side by side. One badly timed sample, one confident wrong rule.

Two consequences, both worth checking before you script anything.

--all is not optional. On my machine the default listing returned 4 rows and claude agents --json --all returned 23. Every one of the 19 hidden rows was a background session whose process had exited, which is to say every worker I might want to check on.

A filter on .status drops finished workers, not all workers. That is worse than dropping all of them, because the result is plausible rather than obviously empty. Compare the two counts yourself:

bash
claude agents --json       | jq 'length'
claude agents --json --all | jq 'length'

If you script this, pass --all, branch on kind, and read state for background rows. One more reason for that last part if your script polls the instant it launches something: for roughly the first second status reads idle on a worker that is already working, so an immediate check sees an idle worker that does not exist. It settles after that. Checking which field actually carries the number is the same discipline I apply to completion claims generally.

What breaks when you run parallel Claude Code sessions?

Five things surprised me. Four are documented behaviour rather than bugs, which is the problem: documented is not the same as expected.

Check these before you leave a worker unattended
It isolates on first EDIT, so a read-only worker stays in your tree
v
In a worktree it entered itself, it commits without asking
v
A bypass-permissions receiver may hold incoming messages
v
Repeated identical messages are dropped by design

It isolates itself, but only when it writes

This one nearly went into the post backwards, and the shape of my mistake is worth more than the fact.

A background session starts in your working directory. Before it edits a file, it moves itself into an isolated git worktree under .claude/worktrees/ on its own branch. So parallel sessions read the same checkout and each writes to its own.

The trigger is the first edit, not the launch. That is the whole subtlety, and it is why my first test got the wrong answer: I asked a worker to report pwd, its branch and git status, and it truthfully reported my directory and my branch, because a reporting task never edits and the move never fired. I nearly published "background sessions do not isolate themselves" on the strength of it.

The second test asked the worker to edit first, then report. Same throwaway repository, on feature/live-branch, with one uncommitted change of my own:

code
worker pwd       /private/tmp/wt2/.claude/worktrees/worker-edit
worker branch    worktree-worker-edit
git worktree list
  /private/tmp/wt2                                77d5439 [feature/live-branch]
  /private/tmp/wt2/.claude/worktrees/worker-edit  77d5439 [worktree-worker-edit] locked

launch tree  tracked.txt   ORIGINAL / DIRTY-EDIT-FROM-ME     <- worker's edit absent
worktree     tracked.txt   ORIGINAL / WORKER-EDIT            <- landed here instead

Two useful facts fall out of that, with one condition attached. The isolation is not unconditional: it is skipped when isolation is set to none, when the session started inside a worktree that already existed, and when the directory is not a git repository, where there is nothing to isolate into. Where it does happen, your working tree is safe: the worker wrote to its own branch and never touched mine. And the worker branched from the committed state, so it never saw my uncommitted change. If you dispatch a worker to finish something you have half-written and not committed, it is working from a version of the file that does not include your work. Commit or stash first.

The mistake worth copying

When a documented behaviour triggers on a specific action, the test has to perform that action. Mine tested an adjacent one, saw nothing, and I read that as absence of the behaviour rather than absence of the trigger. Before publishing "X does not happen", name the documented trigger for X and confirm your test actually pulled it.

In a worktree it entered itself, it commits without asking

The auto-preservation behaviour is real but narrower than it sounds. It applies when the session has made code changes in a worktree Claude entered on its own. Then it commits without asking and pushes the branch if a remote exists, and it opens a draft pull request when the task calls for it. The documented never list is pushing to main or master, force-pushing, and merging.

A session editing a checkout it did not isolate itself still asks first, which covers the common case above and also the case where you launched it inside a worktree that already existed. Your own git instructions in the task, in CLAUDE.md or in memory take precedence over all of it.

Read that last sentence carefully, because it cuts both ways. These are instructions to the model, not enforced guards. One of my background sessions merged a pull request I had not asked for, which is outside the documented behaviour entirely. A "do not push" line in your prompt is a request, and requests get ignored.

Messages can be held instead of delivered

Inbound handling depends on the two sessions' permission modes. A receiver that bypasses permission prompts holds incoming messages for your approval, unless the sender also identifies itself as bypassing, in which case they go through. The approval dialog expires after five minutes by default. A background session with no terminal attached leaves the dialog open past that deadline, and after you attach the message is dropped only if it goes unanswered for another full deadline period. If you want an unattended worker to take messages, set crossSessionInbound to accept in its settings.

There are two separate caps worth knowing, because they are easy to conflate: at most 50 accepted messages queue for Claude to read, and at most 100 held messages wait for your approval. Different queues, different limits.

The feature can be off without saying so

Cross-session messaging needs v2.1.224 or later, runs on macOS and Linux including WSL 2, and is not offered on native Windows. It is also unavailable on Bedrock, Claude Platform on AWS, Google Cloud's Agent Platform and Microsoft Foundry. Several privacy environment variables that switch off feature-flag evaluation, among them DISABLE_TELEMETRY and DO_NOT_TRACK, turn the whole feature off silently. The diagnostic is one command: if /list-agents is not recognised, the session does not have it.

Loops are throttled on purpose

Identical repeats inside a short window get dropped and sends are rate limited per sender, on top of the two queue caps above. Two sessions cannot talk each other into an infinite loop, which is reassuring, but it also means a retry that looks ignored may simply have been deduplicated.

The parallel Claude Code sessions workflow I actually use

One session in front, workers behind it
ArtifactsA result file per worker. This is the completion signal, not the status field
Background workersclaude --bg --name <slug>, each with its own context and its own result file
Main sessionLaunches, messages and collects. Calls ListAgents and SendMessage for you
YouOne terminal. You never open a worker's window

Five rules keep it boring, which is the goal:

  1. Every launch prompt names a result file. No file, no completion signal. claude logs prints a raw terminal dump that is painful to read, so the file is not optional.
  2. Commit or stash before dispatching a writing worker. It isolates itself before it writes, branching from the committed state, so anything you have not committed is invisible to it. That is the failure I would actually hit: a worker doing careful work on the wrong version of a file.
  3. Two unattended workers, not five. This is a discipline, not a limit Claude Code enforces. You can count the live ones, since pid is only present while a process is alive: claude agents --json --all | jq '[.[]|select(.pid)]|length' returned 4 against 26 rows on my machine just now. What nothing does is stop you at a number.
  4. One session stays in front. I read every result in my main session rather than opening worker windows. If I have to attach to a worker to know what happened, the launch prompt was underspecified.
  5. Never for anything irreversible. No credentials, no migrations, no repository I am mid-edit on in the front session.

The delegation scoring that decides what is worth a worker at all, plus the hooks that record what came back, live in primeline-ai/evolving-lite. The tmux layer this replaced is still public at primeline-ai/claude-tmux-orchestration, and it still earns its place if you want every worker visible in one pane at once. That is the one thing agent view does differently rather than better.

How do you hand over the orchestrator context?

The session in front fills up fastest, because every worker's report lands in it. Then you compact or rewind, reclaim the context, and lose the thread with it. The replacement re-derives what it already knew, re-reads files it already read, and asks you questions you already answered.

A handoff note fixes that, and there is one rule that decides whether it helps or hurts.

Write down only what survives the rewind

Commit SHAs, PR numbers, task ids, file paths, session ids: these exist outside the conversation and are still true afterwards. "The decision was", "the reasoning went like this", "I checked it and it is fine" do not. A fact that lives only in the transcript is not state, it is a memory about to be deleted, and writing it into a handoff is how a claim outlives the evidence for it.

Seven blocks earn their place. Everything else is the transcript trying to survive.

BlockWhat goes in it
Role and boundaryWhat this session is, and explicitly what it may not do
What is liveEach running thing, with the command to re-check it
Work itemsBy ID, with current state
Your decisionsOne line each, with a recommendation
Durable pointersIDs and paths, not prose
Traps already paid forSo the cold session does not buy them twice
Next actionOne line

The prohibitions matter more than the permissions. A fresh session with authority and no boundary is the one that does damage, because it does not know which parts of the repository are somebody else's.

Four rules make the difference between a handoff that helps and one that misleads:

  1. IDs over prose. A knowledge-node id is eight characters. The reasoning behind it is four hundred words. Point at it rather than restating it. That is the whole context saving.
  2. Every SHA is stale on arrival. Ship the re-query command next to the value, never the value alone.
  3. Regenerate, do not patch. A handoff edited three times is one nobody trusts.
  4. State what the session may not touch, by path.

Rule 2 is the one I would push hardest, and this post is the evidence. Its own pull request head moved 8 times while it was being written, and that count was out of date twice before this sentence was finished. Any note taken at any point in that sequence carried a head SHA that was wrong within the hour, while the rest of the note stayed perfectly accurate. That is exactly what makes the stale line dangerous: nothing in the document marks which field has rotted. A gh pr view <n> --json headRefOid next to the value costs one line and removes the problem.

/whats-next in primeline-ai/evolving-lite writes a handoff of this shape for a reader who was never in the session. The version below is the other case: distilling a session so its own replacement can carry on, which is what you want before a /rewind.

The skill, if you would rather not write it yourself

Two lines to install, then paste the file:

bash
mkdir -p ~/.claude/skills/compact-stuff
# paste the SKILL.md below into ~/.claude/skills/compact-stuff/SKILL.md

One caveat that catches exactly the person most likely to run that as written. Claude Code watches your skills directories and picks up a new skill in the session you are already in, no restart. But if you just created ~/.claude/skills/ for the first time, which is why the -p is in that command, it was not being watched when your session started. Restart Claude Code once so it begins watching. Skip that and you paste the file, type /compact-stuff, get nothing, and conclude it is broken.

markdown
---
name: compact-stuff
description: Distill this session's durable knowledge to the clipboard before a /rewind or /compact, so the next context starts informed instead of blind. Use when context is running out, before rewinding, or when the user says "compact stuff", "compress context", "reclaim context".
---

# Compact Stuff

Before a `/rewind` or `/compact` throws away this conversation, extract what is worth
carrying and put it on the clipboard, so it can be pasted back into the fresh context.

`/compact` writes its own summary. This is deliberately not that. A compaction summary
describes *the conversation*. This describes *the state of the work*, which is a smaller
and more useful thing.

## The one rule that decides what goes in

**Write down only what survives the rewind.**

Commit SHAs, PR numbers, issue ids, file paths, process ids, ports, memory-node ids -
those exist outside the conversation and are still true afterwards. "The decision was", "the
reasoning went like this", "I checked and it is fine" do not. If a fact lives only in the transcript,
it is not state - it is a memory about to be deleted, and copying it forward is how a
claim outlives its own evidence.

Where a durable store already holds the reasoning, **point at it, do not restate it**.
An id is a few characters; the reasoning behind it is several hundred words. That
substitution is the entire context saving.

## Step 1 - distill

Read back over this conversation and extract, in this order:

1. **Task state** - what is done, what is in progress, what is next. One line each.
2. **Decisions** - what was decided *and why*. "Chose X because Y", never bare "Chose X".
3. **Solutions** - what was fixed and what the root cause was. "Fixed Z by W, cause was V".
4. **Key paths** - files and directories that matter. The next context does not know them.
5. **Live state** - anything running: background sessions, dev servers, ports, pids. Include
   the command that re-verifies it, not just the value. Values go stale; commands do not.
6. **Traps** - what cost time this session, so the next context does not buy it twice.
7. **Open decisions for the user** - one line each, with your recommendation.

Be brutal. If it is obvious, or cheaply rediscoverable, cut it. Five hundred words of
state beats two thousand words of narrative.

**Every identifier you write down is stale on arrival.** Put the re-query command next to
it - `git log --oneline -1`, `gh pr view <n> --json headRefOid` - so the next context can
check rather than trust.

## Step 2 - write it, copy it, verify it

Replace the placeholder lines with your real distilled content. Do not run the template
as-is; the guard below exists because that is the failure that actually happens.

```bash
BACKUP="${TMPDIR:-/tmp}/compact-stuff-$(date +%Y%m%d-%H%M%S).md"

cat > "$BACKUP" << 'EOF'
## Session state

### Task state
- [replace me]

### Decisions
- [replace me]

### Solutions
- [replace me]

### Key paths
- [replace me]

### Live state (with re-verify commands)
- [replace me]

### Traps already paid for
- [replace me]

### Open for the user
- [replace me]
EOF

# clipboard, cross-platform
if   command -v pbcopy  >/dev/null 2>&1; then COPY=pbcopy;              PASTE=pbpaste
elif command -v wl-copy >/dev/null 2>&1; then COPY=wl-copy;             PASTE="wl-paste"
elif command -v xclip   >/dev/null 2>&1; then COPY="xclip -selection c"; PASTE="xclip -selection c -o"
else COPY=""; fi

if [ -n "$COPY" ]; then
  $COPY < "$BACKUP"
  CHARS=$($PASTE 2>/dev/null | wc -c | tr -d ' ')
  LEFTOVER=$($PASTE 2>/dev/null | grep -c 'replace me' || true)
else
  CHARS=$(wc -c < "$BACKUP" | tr -d ' ')
  LEFTOVER=$(grep -c 'replace me' "$BACKUP" || true)
  echo "NOTE: no clipboard tool found (pbcopy/wl-copy/xclip). File only."
fi

if [ "$LEFTOVER" -gt 0 ]; then
  echo "FAIL: $LEFTOVER placeholder line(s) still present - the template was not filled in."
  echo "      Nothing useful is on the clipboard. Redo step 1."
elif [ "$CHARS" -lt 200 ]; then
  echo "FAIL: only $CHARS chars - too thin to be worth pasting back."
else
  echo "OK: $CHARS chars ready. Backup: $BACKUP"
fi
```

The guard is the point. A distillation skill whose failure mode is silently copying its
own template would hand you an empty context and look like it worked.

## Step 3 - tell the user how to land it

Print the distilled text so it can be read before the rewind, then:

```
COPIED. To land it:
  1. /rewind  (or Esc Esc)
  2. Pick the message to rewind to - usually just after setup and exploration
  3. Choose Delete, not Summarize. You already have the better summary.
  4. Paste.

Backup if the clipboard is lost: <path printed above>
```

Choose **Delete** rather than **Summarize** deliberately: a summarize pass spends tokens
regenerating a worse version of what is already on the clipboard.

## When not to use this

- Context is comfortable. This costs a turn; spend it when the turn buys something.
- The work is already written down. If every decision is in commits, issues or a memory
  store, the next context can read those. Distil the pointers, not the prose.
- You are about to end the session rather than continue it. Then you want a handoff
  document, which is a different shape: written for a reader who was never here.

Two notes on that file, in the spirit of the rest of this post.

What is tested and what is not. The guard is tested three ways: an unfilled template fails on seven placeholder lines, real content passes, and a stub of forty characters fails as too thin. That first case is the one that matters, because it is the exact failure the guard exists to catch and I confirmed it firing rather than assuming it.

The load path is proven too, though not by the obvious check. This machine has 26 skills sitting at exactly that path with no manifest at all, and every one of them is in the live roster and invocable. So a bare SKILL.md under ~/.claude/skills/<name>/ loads. Note what does not establish that: claude plugin list returns nothing for these, because a bare skill directory is not a plugin and that command only lists plugins. The obvious check is the wrong instrument, and reading its silence as "it did not load" is the same mistake as reading a missing worktree as "it does not isolate".

What is still untested is this particular skill end to end: one full run, distilling a real session and landing on the clipboard. So the layout is proven and the guard is proven; the whole thing working start to finish, on your machine, is not something I can claim for you.

Two things came out of my own version. It read a state file and a context-percentage file that only exist on my machine, both with 2>/dev/null || echo <default>, so on your machine they would fail silently and hand you a default. Neither value was consumed by any later step, so they were dead code rather than a porting problem, and they are gone. That is the same defect this post keeps circling: an instruction that looks fine and quietly does nothing.

The pattern still matters more than the file. Writing a handoff by hand the first few times is how you find out which of your "facts" only ever existed in the conversation.

Honest scope

n=1 developer, one macOS machine, Claude Code v2.1.233, measured on 2026-08-15. The lag is a single observation at three second polling resolution, which bounds it to somewhere between three and nine seconds rather than pinning it. The 4-versus-23 row counts are one machine's listing at one moment and will not be your numbers; the direction is the point, not the size. I have not checked any of this on Linux or across upgrades, and Claude Code ships changes weekly, so run claude agents --json --all and /list-agents against your own install before building on it.

Three claims in earlier drafts of this post were wrong, and all three are corrected above rather than quietly patched. I wrote that background and interactive rows carry disjoint fields, which was an artefact of sampling only processes that had already exited. I described the auto-commit trigger more broadly than the documentation supports. And worst of the three, I wrote that a background session does not isolate itself into a worktree, on the strength of a test whose worker never edited anything.

All three are the same mistake wearing different clothes, which is the reason this post keeps circling back to it: I measured something adjacent to the claim and read the result as if it settled the claim. Twice an external model had to point it out. If you take one thing from this post, take that rather than any of the commands.

Still untested: whether a blocked worker recovers on its own. The documentation pairs blocked with waitingFor, whose values include permission prompt, and the practical reading is that an unattended worker sitting there waits indefinitely, with claude attach <id> as the way out. I have watched one wait; I have not watched one time out.

FAQ

How do I run multiple Claude Code sessions in parallel?+
Start each one with claude --bg --name <label> and a prompt. Each is a full session with its own context that keeps running without a terminal. Use claude agents to see them in one screen, or claude agents --json --all for scripting.
How do I get a background Claude Code session to report back?+
Ask your main session in plain English, for example 'ask api-worker whether the migration finished'. Claude uses the ListAgents and SendMessage tools itself. You never call them directly. From v2.1.232 you can also name the target with an @ mention.
What is the difference between a subagent and a background session?+
Both get their own context window, their own hooks and their own MCP servers, so those are not the difference. A subagent returns one report into your conversation and is finished. A background session is a separate conversation that survives you closing the terminal and stays addressable across several rounds.
Why does my background session still say working after it finished?+
The state field describes the last polled instant, not the artifact. In my run the output file existed at the six second poll while state still read working at nine seconds, so the lag was between three and nine seconds. Treat the artifact the worker produces as the completion signal.
Can I send a second task to a Claude Code session that is already done?+
Yes. A finished background session keeps its conversation and context, and a peer message starts a new turn in it. I sent a second instruction into a session reading state done and it acted on it within two seconds.
Why does my filter on status miss background sessions?+
Because pid and status are only present while the process is alive, so they vanish from a finished worker's row. Also pass --all: my default listing showed 4 rows and --all showed 23, and every hidden row was a background session that had exited.
Does claude --bg create its own git worktree?+
Usually, but not at launch and not unconditionally. It starts in your working directory and moves into an isolated worktree before it edits a file, so a read-only worker stays in your tree the whole time. The move is skipped when isolation is set to none, when the session started inside an existing worktree, or when the directory is not a git repository at all. Check rather than assume, and commit or stash first either way, because an isolated worker branches from the committed state and cannot see your uncommitted work.
Will a background Claude Code session commit and push on its own?+
Only when it made changes in a worktree it entered itself. Then it commits without asking and pushes when a remote exists. The documented never list is main, master, force-push and merging. Treat that as instructions to the model rather than an enforced guard: one of my background sessions merged a pull request I had not asked for. A session editing a checkout it did not isolate asks first.
Why can my sessions not message each other?+
Type /list-agents. If the command is not recognised the session lacks the feature: check for v2.1.224 or later, macOS or Linux, and whether DISABLE_TELEMETRY or DO_NOT_TRACK is set, since those switch it off silently. If it works but nothing arrives, check the receiver's crossSessionInbound setting.

>_ Get the free Claude Code guide

>_ No spam. Unsubscribe anytime.

>_ Related