Isolated Sessions

Updated September 18, 2026

Isolated sessions let you run multiple coding agents (Claude Code, Codex, or any agent you run in a shard) on the same repository simultaneously, each on its own git branch. Under the hood, crystl uses git worktrees to give each agent a separate working copy, either on your Mac or on a remote server over SSH. New to worktrees? Start with our complete guide to git worktrees with Claude Code.

How it works

When you create an isolated shard, crystl:

  1. Creates a new git worktree from the gem’s integration target (the repository default branch by default)
  2. Detects your package manager and installs dependencies
  3. Symlinks .env files from the main project
  4. Tells the agent it’s in an isolated worktree (via system prompt) so it stays scoped to its task
  5. Opens a terminal session in that worktree directory
  6. Marks the shard with a branch icon so you can tell it apart from regular shards

Each isolated shard has its own copy of the repo on its own branch. Agents in different isolated shards can edit the same files without overwriting one another’s live checkout; if they change the same lines, git may still report a conflict when you integrate the branches.

Creating an isolated shard

Click the worktree row (the branch icon in the column that drops when you hover the “+” in the shard bar, also on the File menu) to spin up a new isolated shard in a fresh worktree, forked from the gem’s integration target (the repository default branch by default). crystl names the branch, opens the worktree, and drops you into a ready agent session.

Forking from a branch

Control-click (or right-click) the worktree row to open the branch picker: fork the worktree from an existing branch instead of the default, or start a new branch from a checkout of your choice. Use this to base a shard on a release line, a feature branch, or a teammate’s work rather than main.

Reopening a parked worktree left behind by a closed shard is a separate path: the branches button in the shard bar; see Reopening worktrees.

Via the CLI

From any shell, or from inside another shard’s agent session, use the crystl CLI to create an isolated shard without touching the GUI:

crystl shard create --gem myapp --isolated                       # auto-named worktree
crystl shard create --gem myapp --isolated --crystal-name jade   # pick the name
crystl shard create --gem myapp --isolated --prompt "fix #42" # seed it with a task
crystl shard create --gem myapp --isolated --prompt "fix #42" --scrollback 500 # cap scrollback for a light worker

Useful for orchestrator agents that want to fan parallel work out into worktree-isolated siblings without waiting for a human to click.

Local vs. remote isolation

An isolated shard runs wherever its gem is currently working. Any shard in crystl falls into one of four states:

State Shell Git worktree How to create
Local Your Mac None Click “+” in a local gem
Local isolated Your Mac On your Mac Isolated-shard row in a local gem
SSH’d Remote server None Open an SSH gem or shard
Remote isolated Remote server On the remote server Isolated-shard row in a gem with an active SSH shard

When you click the isolated-shard row in a gem that already has an SSH shard, crystl detects the existing remote session and creates the worktree on the remote server instead of locally. You get all the isolation benefits (parallel agents, branch tracking, merge-on-close) without pulling the repo to your Mac first.

Remote isolated shards

Remote isolation piggybacks on crystl’s existing SSH ControlMaster connection to run git commands on the remote host.

Creation flow

When you click the isolated-shard row in a gem that has a live SSH shard, crystl:

  1. Detects the SSH shard in the current gem and reuses its RemoteSession (ControlMaster socket).
  2. Finds the remote git root by running git rev-parse --show-toplevel over SSH from the current remote working directory.
  3. Asks the host which crystals it already holds and counts up from the name it was given until one is free: amber, then amber-2, then amber-3. The name is allocated from your Mac, but the branches that matter are on the server, so crystl checks there before it creates anything. That is why the isolated-shard button works on a remote gem without you choosing a name. A name you typed yourself, and a reattach of a shard you named, are left alone: each means that exact shard.
  4. Creates the worktree remotely:
    git worktree add -b crystl/{crystal-name} .crystl/worktrees/{crystal-name}
  5. Symlinks config files on the remote so the agent picks them up: CLAUDE.md, AGENTS.md, .mcp.json, .claude/.
  6. Writes an autorun.sh into the worktree’s own .crystl/ folder on the remote. The autorun wraps the agent command with its system-prompt flag (for Claude Code, claude --append-system-prompt) so the agent knows it’s isolated (see Agent Context).
  7. Opens a new SSH shard to the same host and cds into the remote worktree path.
  8. Tracks remoteWorktreePath and remoteProjectDir on the session so merge/close operations can find the right files later.

The new shard behaves like any other SSH shard (command history, approval cards, click-to-open) but its git state lives on the remote.

When a remote create cannot go ahead

A remote create that is refused says why. Some refusals, like a directory that is not a git repository or a stale leftover branch, close the placeholder shard and report the reason as a card. Others, like a worktree creation that failed partway, keep a working shell open with the reason written into it. Either way a shard never appears and vanishes with nothing said.

A create that stalls is bounded. crystl gives the host 30 seconds, and 10 on the first connection check, then stops waiting and reports the reason.

Non-git remotes

If git rev-parse --show-toplevel fails on the remote (not a git repo), crystl refuses the isolated shard and tells you why: isolated shards need git on the remote too. Open a regular SSH shard with the normal “+” instead if a plain session is all you need.

Agent context

When your agent starts in an isolated shard (local or remote), crystl injects isolation context through the agent’s system prompt. For Claude Code, that goes in via --append-system-prompt and reads:

You are working in an isolated worktree on branch {branch}. Other agents may be working on the same repo in parallel. Keep your changes scoped to your task. IMPORTANT: Do not merge, cherry-pick, or rebase your branch into the integration branch yourself. Do not push to or modify protected branches. Crystl handles branch integration when the shard closes.

The prompt also names the destructive git commands (git reset --hard, git clean -fdx, branch-changing checkouts, worktree removal) that would destroy the session, so the agent knows not to reach for them. You don’t need to add any of this to your prompt. The branch name is substituted in at session start, so the agent knows exactly which worktree it’s in. That reminder helps avoid agents cherry-picking commits, rebasing onto main, or pushing to the wrong branch; the prompt itself is guidance, not enforcement.

The write guard

The system prompt above asks the agent to stay in its worktree. The write guard adds a hook-based enforcement layer for the file operations crystl can identify before they run. It is not an OS filesystem sandbox: the agent process still has your user’s normal filesystem access, and the boundary is only as broad as the supported hooks and tool payloads described below.

Path confinement covers these structured file-writing tools: Claude Code’s Write, Edit, MultiEdit, and NotebookEdit, plus Codex’s apply_patch. When one of those tools targets a path inside the repository but outside the isolated shard’s own worktree, crystl denies it before the normal approval flow, including in automatic approval modes. Each path in a multi-file call is checked independently, so one in-bounds file cannot hide an out-of-bounds file in the same call.

Arbitrary Bash writes are not generally path-confined. Commands such as cp, shell redirection, sed -i, package installers, or a script can write anywhere your user account can write. There is a separate, deliberately narrow Bash self-destruction guard for the isolated shard’s own worktree. It blocks recursive forced removal of the worktree or current directory (rm -rf), git clean -fdx, git reset --hard, branch-changing git checkout / git switch (including whole-tree checkout discards), and git worktree remove, move, or prune when they would remove, move, or invalidate the current session’s worktree. It is a targeted command matcher, not general shell containment.

Only the Claude Code and Codex integrations currently enforce these denials: their pre-tool hooks return the bridge’s deny response to the agent. Antigravity’s current hook adapter does not pass the bridge’s denial back to the agent, and an arbitrary CLI agent running in a shard has no enforcement unless it implements a compatible denying hook. The injected isolation instructions still apply to every agent, but instructions are not a security boundary.

For structured writes, paths are resolved the way the filesystem resolves them, not by string matching. That matters because a .. inside a symlinked directory applies to the link’s target: worktree/link/../file looks in-tree to a lexical check while the filesystem lands it somewhere else, and a plain worktree/link → repo/file needs no .. at all. Local hooks resolve against the local filesystem. Remote hooks resolve paths on the remote filesystem before the bridge checks containment, so a symlink on the server is judged by its server-side target rather than by the Mac running crystl.

The structured-write check fails closed when it cannot establish a target: a broken or looping symlink, a path it cannot resolve to a real location, or an envelope that names no file at all (a malformed tool_input, an apply_patch with no file markers). The denial says which file and why, so your agent can correct itself instead of guessing.

The managed config links still work. crystl symlinks CLAUDE.md, AGENTS.md, .mcp.json, and .claude/ from the gem root into every worktree by design, and writes through them resolve to the gem root, in-repo but outside the worktree. Those are allowed, on a deliberately narrow rule: the link has to be one crystl created, be a real symlink at the worktree root, point at the same-named entry in the gem root, and the write has to land at or under that target. There is no blanket “it looked in-bounds” exemption, because that is exactly the hole a worker-created link out of the worktree would walk through.

Structured writes to paths outside the repo entirely are not blocked by the worktree-containment rule, and neither is reading a credential file. Both still matter after the fact, which is what the touches tab of the agent activity panel records.

Use cases

Parallel feature work

Give two agents different tasks on the same repo:

  • Shard A (isolated): “Add user authentication to the API”
  • Shard B (isolated): “Write integration tests for the payment flow”

Both agents work at full speed without stepping on each other’s changes.

Explore vs. implement

Use one isolated shard for an agent to prototype an approach while you continue working in your main shard. If the prototype looks good, merge it in. If not, discard the worktree.

Code review assistance

Create an isolated shard to have an agent review and suggest improvements to a feature branch while you continue developing on another.

Automatic workspace setup

crystl detects your package manager from lockfiles and runs the install command when the workspace opens. .env files are symlinked from the main project so they’re available immediately.

Supported package managers: npm, pnpm, yarn, bun, pip, poetry, uv, pipenv, bundler, composer, cargo, go, mix, swift, and cocoapods.

Custom setup

For projects that need additional setup steps (starting Docker containers, running migrations, etc.), add a setupCommand to .crystl/project.json:

{ "setupCommand": "docker compose up -d" }

crystl runs the custom command after dependency installation.

Reopening worktrees

When you close an isolated shard with Keep Branch, the branch is preserved in git history: locally for local isolated shards, on the remote server for remote isolated shards. Reopen it later from the branches button at the top-right of the shard bar, which opens the worktrees panel (below).

The worktrees panel

The branches button opens a “{gem} worktrees” panel, a single place to see all of a gem’s isolated work. It has two sections:

  • ACTIVE: the isolated shards running right now, one rich row each: what the shard is doing (the task it was created with), its branch, the agent and model driving it, how long since it last did anything, and its commit count. Click a row to bring that shard forward.
  • PARKED: orphaned crystl/* branches with no live shard: parked work from closed shards. Each row shows its commit count, whether it has uncommitted changes, and its last-commit age, plus three actions:
    • reattach: reopen the worktree as a shell so you can resume. It does not auto-start an agent; you land at a prompt in the branch and drive from there (for example, run your agent with its resume flag).
    • merge: integrate the parked commits into the gem’s integration target. Shown only when the branch has commits. Never forces: a conflict is reported and left for you.
    • discard: delete the branch and its commits, after a confirmation.

Merge and discard results appear as a notification, and the button flashes a green check on success, so nothing is written into an unrelated shard’s terminal. The panel scrolls internally when the list is long.

The panel appears only when there’s isolated work to show. If the worktree directory still exists (e.g., after a crash or restart), reattach reuses it as-is, so your uncommitted changes are preserved. If only the branch remains, crystl recreates the worktree from it.

Remote orphaned branches

When the current gem has an active SSH shard, the branches button also lists orphaned crystl/* branches found on the remote server. crystl queries them asynchronously over the existing SSH connection, so the list opens immediately and populates remote entries as the query returns.

Reattaching a remote orphaned branch reopens it on the remote: if .crystl/worktrees/{name} still exists on the server, it’s reused as-is; otherwise crystl recreates the worktree from the branch. Either way, the new shard SSHs back into the same host and lands in the worktree directory.

Leftover branches and fresh names

Crystal names come back around; the crystl/<name> branches they leave behind do not. A shard that closed weeks ago leaves its branch on disk, and the next shard handed that name used to start from that old tip. A worker could open thousands of commits behind, and merging it would revert everything that landed in between.

A new isolated shard never adopts a leftover branch. crystl keeps names whose branch already exists out of a new shard’s reach and hands it the next free name instead, counting up through the roster with a numeric suffix. So you may see amber-2 where you expected amber. The suffixed name keeps amber’s signature color and reads as the shard you asked for. The old branch is left exactly as it is, every commit on it preserved and reopenable from the branches button.

The one leftover branch a new shard still reuses is one that already contains the gem’s integration target, since resuming it cannot start the shard behind. When crystl cannot measure the distance, it treats the branch as unsafe and takes a fresh name.

Reattaching means the branch you named, so the paths that name one still get it: the branches button, resurrect, a restored formation, and a --crystal-name you typed yourself. A parked isolated shard comes back on its own work however long the arrangement sat.

An existing intact worktree directory is still reused as-is on those paths, so uncommitted changes survive.

Your work is safe

Worktrees make people nervous: parallel branches, background agents, and a shard that vanishes when a process exits. The rule crystl follows is simple, and it never bends: crystl only deletes a branch or worktree when it is certain there is nothing to lose.

Concretely, when a shard closes or you discard a worktree, crystl checks the worktree before it removes anything, and refuses to destroy it if any of these are true:

  • The branch has commits. A branch that is even one commit ahead of its fork point is kept. crystl removes the working directory but leaves the branch in git, where the branches button can reattach, merge, or discard it later.
  • The worktree has uncommitted changes. A dirty worktree, tracked or untracked edits, is left on disk untouched. Uncommitted work lives only in the directory, so crystl never force-removes a dirty one. It shows up in the Isolation panel, and reopening reuses the directory with your changes intact. (crystl’s own metadata under .crystl/ does not count as work.)
  • crystl cannot verify what it is about to delete. If it cannot count the branch’s commits, confirm the folder is one of its own managed worktrees, or match the branch to the path git has registered, it preserves everything and reports the refusal rather than guessing.
  • The shard’s agent is still running. crystl waits for the agent process to exit before it touches the worktree, so a still-working agent’s files are never pulled out from under it.

The only path that deletes commits is choosing Discard explicitly. Every other close (Keep Branch, a clean shard with no commits, a process exiting on its own) either merges your work or parks it for recovery. There is no code path that silently drops committed or uncommitted work.

If a worktree ever refuses to clean up, that is by design: something about it could not be verified as safe to remove. It stays parked in the Isolation panel, and you can inspect or discard it deliberately.

Integration target

By default, isolated shards fork from and merge back into your repository’s default branch (main). You can change that per gem: point a gem at a release or feature branch, and every isolated shard and sealed quest for that gem forks its worktree from that branch and merges back into it. This keeps a fleet of workers integrating onto the right line instead of main.

Set it three ways:

  • Global default: settings, worktrees. New gems inherit this.
  • Per-gem override: a gem’s settings, in the worktrees tab.
  • CLI: crystl worktree target shows or sets it, per gem or global.

The choices are the repository default branch, the branch checked out when the shard starts (current), or a specific branch. A sealed quest can also create a fresh branch per run. Because a shard forks its worktree from the target, its commits stay relative to where they will merge, so a later rebase does not replay unrelated history onto the target branch.

Merging back to main

When an isolated shard’s work is complete, there are three ways to merge. The shard-tab badge and the close prompt work identically for local and remote isolated shards: for remote shards, crystl runs the same git commands over SSH against the remote repository. The CLI merge is local-only today.

From the shard tab

An isolated shard with unmerged commits shows a small ↑N merge badge below its tab, tinted in the shard’s color, and the tab’s underline accent brightens while work is waiting. The count is the number of commits ready to integrate. Click the badge to open the merge and rebase actions.

Every action names exactly what it will touch: you’ll see merge crystl/opal into main (3 commits) rather than a bare “merge”, so there’s no guessing which branch is about to move where. Choose the merge action and crystl rebases the shard’s branch onto the target, fast-forward merges, and cleans up the worktree automatically. The shard closes after a successful merge.

The rebase action brings the integration target’s latest local commits into the shard without merging. Useful when you’re still working but want to stay up to date.

From the CLI

crystl merge --gem myapp --shard opal merges an isolated shard’s branch through the same machinery, so an orchestrator can integrate a finished worker without touching the GUI. It returns a structured result (success, nothingToMerge, dirty, conflicts, failed, and notIsolated are the ones you’ll meet) and never forces; conflicts are left for you to resolve from the shard tab’s merge badge. It works on local isolated shards; merge a remote shard from its tab or close prompt instead. Guild-gated, like the other control commands. See the CLI reference.

On shard close

When you close an isolated shard that has commits, crystl prompts you:

  • Merge to Main: rebase + fast-forward merge, then close
  • Keep Branch: close the shard, branch is preserved for later
  • Discard: delete the branch and all its commits

For remote isolated shards, the exact same prompt appears, but the git operations run on the remote server over the session’s SSH ControlMaster, against remoteProjectDir and remoteWorktreePath. The worktree directory and branch on the remote are cleaned up when you choose Merge to Main or Discard.

When closing a gem with multiple unmerged branches, you’ll see Keep Branches / Discard All / Cancel.

Process exit auto-close (e.g., when an agent finishes) skips the prompt so it doesn’t interrupt your work in other shards.

Conflict handling

If the rebase encounters conflicts, crystl aborts the rebase automatically and tells you what went wrong in the terminal. You can resolve conflicts manually with git rebase main and then merge when ready. For remote isolated shards, run the resolution commands in the remote shard itself; you’re already SSH’d into the right directory.

Schema change warnings

When multiple isolated shards are modifying database schema or migration files at the same time, crystl warns you before merging. This helps avoid the common problem where parallel migration files conflict: merge one shard first, regenerate migrations, then merge the next.

Troubleshooting

I closed a shard, where did my commits go?

They are on the branch. Closing an isolated shard with commits either merges them (if you chose Merge to Main) or parks the branch (Keep Branch, or a process that exited on its own). Parked branches live in git as crystl/{name} and reappear in the branches button at the top-right of the shard bar whenever unmerged crystl/* branches exist. From there you can reattach, merge, or discard. See Reopening worktrees.

My uncommitted changes seem gone after a restart

They are not deleted. A worktree with uncommitted changes is left on disk exactly as it was. If the shard is gone but the branch remains, open it from the branches button: if the directory still exists, crystl reuses it with your edits intact; if only the branch remains, crystl recreates the worktree from it. crystl never force-removes a dirty worktree, so the edits are still in the directory under .crystl/worktrees/{name}.

The merge won’t go through

crystl merge and the merge badge never force. A merge can come back as:

  • nothing to merge: the branch has no commits ahead of the target.
  • dirty: the worktree has uncommitted changes. Commit or stash them in the shard first, then merge.
  • conflicts: the rebase hit conflicts and was aborted automatically. Resolve them in the shard (git rebase {target}), then merge from the badge. For a remote isolated shard, run the resolution in the remote shard itself; you are already SSH’d into the right directory.

“Not a git repository”

Isolated shards need a git repo, locally and on a remote host alike. A remote non-git directory is refused with the reason; locally, the attempt falls back to a plain (non-isolated) shard with a warning.

Locally you don’t have to go fix it by hand. The refusal arrives as a card with a git init here button that runs git init in the gem’s folder and seeds the first commit, which is what makes the repo ready for a worktree off main. Click ⎇ again afterwards and the shard is created. The same card appears for the nested case below. A shared shard (the + button) needs no git at all, if that’s all you wanted.

The gem sits inside a bigger git repository

crystl refuses an isolated shard when the gem’s git root is some parent directory rather than the gem itself, because the worktree would be created up at that root, outside your project. The refusal card offers git init here, which gives the gem its own repo so you can retry the isolated shard.

The earlier warning in the New Gem flow is deliberately different: use shared shards is the default, while make standalone repo opts into separate Git history. This keeps an ordinary folder inside a larger repository ordinary unless you explicitly need isolated shards for it. See Creating a gem.

A worktree refuses to clean up or discard

This is a safety refusal, not a bug. crystl declines to remove a worktree it cannot verify, for example when the branch and folder no longer match, the path sits outside its managed .crystl/worktrees directory, or the agent process is still live. The worktree stays parked in the Isolation panel. Let the agent finish, or inspect and discard it deliberately from there. See Your work is safe.

A new isolated shard has a numbered name

amber-2 rather than amber means the plain name’s crystl/<name> branch is still on disk, left by a shard that closed earlier. Your new shard takes a fresh name and a fresh branch instead of starting from that old tip, and the old branch is untouched. See leftover branches and fresh names. No manual cleanup needed, and the old branch is still reachable from the branches button when you want it back.