Isolated Sessions

Updated August 4, 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 gitignored .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.

The worktree menu

Control-click (or right-click) the worktree row to open a menu with more ways to start:

  • new isolated shard from branch…: 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.
  • open orphaned worktree… (N): reopen a parked worktree left behind by a previously closed shard. The number is how many are waiting; 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:

StateShellGit worktreeHow to create
LocalYour MacNoneClick ”+” in a local gem
Local isolatedYour MacOn your MacIsolated-shard row in a local gem
SSH’dRemote serverNoneOpen an SSH gem or shard
Remote isolatedRemote serverOn the remote serverIsolated-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. Creates the worktree remotely:
    git worktree add -b crystl/{crystal-name} .crystl/worktrees/{crystal-name}
  4. Symlinks config files on the remote so the agent picks them up: CLAUDE.md, AGENTS.md, .mcp.json, .claude/.
  5. Writes .crystl/worktrees/{crystal-name}/autorun.sh 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).
  6. Opens a new SSH shard to the same host and cds into the remote worktree path.
  7. 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 panels, click-to-open) but its git state lives on the remote.

Non-git remotes

If git rev-parse --show-toplevel fails on the remote (not a git repo), crystl falls back to creating a plain remote shard at the current directory, same as a normal SSH ”+” click. No worktree, no branch, no warning, just a regular remote session.

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 main. crystl handles branch integration when the shard closes.

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 are still worth knowing about 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. Gitignored .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 two ways:

  • The branches button at the top-right of the shard bar, which opens the worktrees panel (below).
  • The worktree row’s open orphaned worktree… menu item (control-click or right-click the button).

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.

Stale branch recovery

When you create a new isolated shard and crystl detects a leftover branch with the same name, it handles it automatically:

  • Existing intact worktree: reused as-is, preserving uncommitted changes
  • Stale branch with commits: crystl reattaches the worktree to the existing branch
  • Stale branch with no commits: crystl prunes the reference and creates fresh
  • No stale branch: creates a new worktree and branch as normal

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. All work identically for local and remote isolated shards: for remote shards, crystl runs the same git commands over SSH against the remote repository.

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 pulls in upstream changes 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, or notIsolated) and never forces; conflicts are left for you to resolve from the shard tab’s merge badge. 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 or the worktree menu’s open orphaned worktree… item: 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. Creating one in a non-git directory falls back to a plain (non-isolated) shard, with a warning locally and silently on a remote host.

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 leftover branch has the same name as a new shard

crystl handles this automatically when you create a new isolated shard. An intact worktree is reused as-is, a stale branch with commits is reattached, and a stale branch with no commits is pruned and recreated fresh. See Stale branch recovery. No manual cleanup needed.