Patterns

Git Worktrees Solve Code Isolation. They Don't Solve Environment Setup.

A worktree isolates the code. The development environment around that code is yours to set up — at the right lifecycle phase.

by NightMe team 10 min read

A worktree isolates the code. The development environment around that code is yours to set up — at the right lifecycle phase.

A worktree gives an agent its own branch, working directory, HEAD, and index — and nothing else. The .env, dependencies, build caches, code indexes, and graphify baseline live elsewhere, and Git never copied them. Reproducing that context in every new worktree is the topic of this guide.

The parallel-agents rationale lives in Running multiple AI coding agents in parallel. The worktree mechanics this article assumes are in /gtw fix one-shot fixes.

What worktrees don't carry

Category Examples
Environment .env, .env.local, project-local config, generated local files
Dependencies node_modules/, virtual environments, module caches, build artifacts
Tooling Shell hooks, language servers, project-specific scripts
AI context Code indexes, code graphs, project memories

Almost everything in this table is gitignored on purpose. Git never sees it, so neither does the new worktree.

Attach temporarily, close when done

The naive answer to "AI tooling in every worktree" is to install every tool per worktree: a Serena project per worktree, a CodeGraph engine per worktree, an MCP server per worktree. Three agents in flight means nine permanent processes. Ten agents means thirty. Most of them sit idle 95% of the time, leak orphan LSP processes across session boundaries, and make each new worktree a fresh round of npm install-style waiting.

The pattern that scales is to give each tool a lifecycle slot in gtw.yml and nothing more:

  • Tools that describe the source tree belong at the worktree boundary — once per fix, never per commit.
  • Tools that track the branch belong at fix (register) and close (deregister).
  • Tools that share a team-wide baseline belong at fix (copy + sync into the worktree) and close (sync back / update).
  • Tools that are purely global (one shell proxy, one binary) never appear in the hook at all.

Bootstrap with gtw hooks

/gtw fix creates a worktree inside a tracked workflow: it writes .nightme/gtw.yml (cwd-scoped workflow identity) and switches the chat's CWD into the new worktree. The lifecycle hook fires at worktree boundaries, not on every commit — the right cadence for environment setup.

The hook schema is phase-specific. Each phase (fix, close, …) has its own before / after slots, and each phase can dispatch a named agent:

# ~/.nightme/gtw.yml
fix:
  agent: "pi"
  hooks:
    after:
      - # run after /gtw fix creates the worktree
        codegraph init

close:
  hooks:
    before:
      - # run before /gtw close tears the worktree down
    after:
      - # run after /gtw close completes

The hook process exports:

Variable Meaning
NIGHTME_GTW_BRANCH (or $GTW_BRANCH) The new branch name (fix/parser-nom)
NIGHTME_GTW_BASE The ref the branch was cut from (main, release/1.x, …)
NIGHTME_GTW_WORKTREE Absolute path to the new worktree
NIGHTME_GTW_REPO (or $GTW_REPO_ROOT) Absolute path to the original repo

The full config surface is in Write your first /gtw config.

Tool configuration

The three tools below are the stack this article is built around: CodeGraph for structural code intelligence, tokensave for branch-aware code indexing, graphify for the project knowledge graph. Pick the ones your project uses; leave the rest.

CodeGraph — structural code intelligence

CodeGraph indexes symbols, imports, callers, callees, impact, and context for the current working directory and exposes structural and semantic tools through MCP.

State location: split.

  • Engine: ~/.codegraph/bin — installed once per machine.
  • Index: per worktree — derived from the worktree's source tree.

Why split: the engine is one binary; the index describes one source tree. A stale index from another worktree is a bug, not a starting point.

Configuration:

Machine-global
~/.codegraph/bin
   = installed engine

Worktree
current worktree
   = indexed code context

gtw hook placement: fix.hooks.aftercodegraph init primes the index for the new worktree so codegraph_explore hits on the fix branch.

fix:
  hooks:
    after:
      - codegraph init

Never copy a CodeGraph index from another worktree. The derived index must belong to the exact source tree the agent is working on.

tokensave — branch-aware code indexing

tokensave indexes the current working directory and exposes structural and semantic tools through MCP. It also tracks branches: each worktree's branch is registered on fix, deregistered on close, and the data is consolidated on close.

State location:

  • Index data: per worktree, keyed by branch. Stored in .tokensave/ (database at .tokensave/tokensave.db, branch metadata at .tokensave/branch-meta.json).
  • MCP server: global, registered once with the agent client.

Why per-branch: tokensave is branch-aware — the same query against main and fix/parser-nom should return different call graphs. Registering the branch at fix and removing it at close keeps the branch lifecycle in lockstep with the worktree lifecycle.

Configuration:

fix
   ↓
tokensave branch add <branch> --path <repo-root>
   ↓
[agent works in this worktree]
   ↓
close (before teardown)
   ↓
tokensave branch remove <branch> --path <repo-root>
   ↓
close (after teardown)
   ↓
tokensave sync

gtw hook placement:

fix:
  hooks:
    after:
      - tokensave branch add "$GTW_BRANCH" --path "$GTW_REPO_ROOT"

close:
  hooks:
    before:
      - tokensave branch remove "$GTW_BRANCH" --path "$GTW_REPO_ROOT"
    after:
      - tokensave sync

Verify the exact flag names against current tokensave docs (tokensave --help, tokensave branch --help) before committing the hook.

graphify — knowledge-graph baseline per worktree

graphify builds a graph from the current project, writes graph.json / GRAPH_REPORT.md / graph.html, supports a .graphifyignore, and serves the graph through MCP.

State location: split.

  • Baseline (graphify_out/) at the repo root: the canonical team-wide graph, built and updated on main.
  • Worktree copy: a per-worktree mirror of the baseline, synced after fix so the agent has the same starting map.

Why copy + sync: every worktree should start from the same team baseline so a query in one worktree returns the same answer as the same query in another. A pure rebuild per worktree diverges — the baseline in main would drift from what each worktree sees.

Configuration:

fix
   ↓
copy <repo-root>/graphify_out → <worktree>/graphify_out
   ↓
cd <worktree> && graphify sync
   ↓
[agent works in this worktree]
   ↓
close (after teardown)
   ↓
graphify update   # refresh the baseline so the next worktree inherits it

gtw hook placement:

fix:
  hooks:
    after:
      - cp -r "$GTW_REPO_ROOT/graphify_out" "$GTW_WORKTREE/graphify_out"
      - cd "$GTW_WORKTREE" && graphify sync

close:
  hooks:
    after:
      - graphify update

Verify the exact subcommand (sync / update) against current graphify docs before committing the hook.

.env and secrets

.env, .env.local, and credential files are gitignored by design.

State location: machine-local secret store (1Password CLI, direnv, doppler, infisical, or whatever the team already uses).

Why not Git: never commit secrets to make worktrees convenient. The convenience is a tax on the security model.

Configuration:

  • Use a project-approved secret source. No per-worktree hack.
  • Materialise non-secret local config in the appropriate phase hook.
  • Make the bootstrap explicit and reproducible — a teammate reading the hook should see exactly what is materialised where.

Combined reference implementation

# ~/.nightme/gtw.yml
fix:
  agent: "pi"
  hooks:
    after:
      # Runs after /gtw fix. codegraph init primes the index for the
      # new worktree so codegraph_explore hits on the fix branch.
      - codegraph init

      # Register the branch with tokensave so branch-aware queries
      # resolve against this worktree.
      - tokensave branch add "$GTW_BRANCH" --path "$GTW_REPO_ROOT"

      # Copy main's graphify baseline into the worktree, then sync
      # so every worktree starts from the same team-wide map.
      - cp -r "$GTW_REPO_ROOT/graphify_out" "$GTW_WORKTREE/graphify_out"
      - cd "$GTW_WORKTREE" && graphify sync

close:
  hooks:
    before:
      # Deregister the branch before the worktree is torn down so
      # tokensave stops tracking it.
      - tokensave branch remove "$GTW_BRANCH" --path "$GTW_REPO_ROOT"
    after:
      # Sync tokensave, then update the team-wide graphify baseline
      # so the next worktree inherits the latest map.
      - tokensave sync
      - graphify update

Verify the exact CLI invocation for each tool against its current docs before committing the hook to the repo.

Lifecycle table

Tool / state fix.hooks.after close.hooks.before close.hooks.after
CodeGraph codegraph init
tokensave tokensave branch add "$GTW_BRANCH" --path "$GTW_REPO_ROOT" tokensave branch remove "$GTW_BRANCH" --path "$GTW_REPO_ROOT" tokensave sync
graphify cp -r ... graphify_out + graphify sync graphify update
.env / secrets materialise from project-approved source

The rule the table encodes: every tool has one or more lifecycle slots in gtw.yml. A slot lives at the earliest phase where its input is available and the latest phase where its output is still useful. No slot lives on every commit.

Anti-patterns

  • A CodeGraph Git hook on every commit / checkout.
  • Copying a CodeGraph index from another worktree.
  • Putting any tool in a Git post-commit / post-checkout hook instead of the gtw lifecycle hook.
  • Putting mcpls or any LSP-grade MCP server in a gtw hook (it scales linearly with parallel agents — attach it on demand instead).
  • Rebuilding the whole graphify_out from scratch inside fix.hooks.after (copy + sync instead).
  • Automatic CLAUDE.md / AGENTS.md mutation merely because a worktree exists.

Checklist

Before using worktrees

  • Decide which tool owns each lifecycle slot in gtw.yml.
  • Define one reproducible bootstrap command chain.

On fix.hooks.after

  • Initialise the per-worktree index (CodeGraph).
  • Register the branch (tokensave).
  • Copy + sync the team baseline (graphify).

On close.hooks.before

  • Deregister the branch (tokensave).

On close.hooks.after

  • Sync the index (tokensave).
  • Update the team baseline (graphify).

Summary

Git         = isolates code
gtw         = manages lifecycle
gtw hooks   = give each tool its lifecycle slot
NightMe     = keeps human + agent + worktree context connected

Git tracks the source of truth. Worktrees isolate it. AI tools get one or more lifecycle slots in gtw.yml — no Git hooks, no per-commit reindex. NightMe keeps the human + agent + worktree context together so the four-layer model is observable, steerable, and resumable from anywhere — not just the terminal.