This year has changed the way I do software development completely. Last year I was using Claude Code but I still used an IDE and did at about 20-30% of the work myself either because I was fixing up Claude’s work or it was small enough that it didn’t seem worth it to spin up an agent.

This year however, I have barely written a single line of code. I don’t even do git commits myself anymore. I’ve completely dropped my IDE and I just work in the terminal with Claude, neo-vim and lazygit.

Another thing that’s changed is that I’m routinely working on multiple things at the same time. I was doing this last year too but the IDE was a bottleneck since I didn’t want to have to start a new IDE window for each parallel project. Now that I don’t use an IDE it doesn’t matter.

Enter worktrees

I’ve found that Git worktrees are essential to doing parallel developement. I almost never do work in the main repo root anymore because it makes creating new worktrees harder. Managing all these worktrees was becoming a headache, especially setting up the environment each time. I started using worktree-cli along with a worktrees.json file to handle the project setup:

{
  "setup-worktree": [
    "cp $ROOT_WORKTREE_PATH/.env .env 2>/dev/null || echo 'No .env found in main worktree; skipping'",
    "cp $ROOT_WORKTREE_PATH/.envrc .envrc 2>/dev/null || true",
    "cp $ROOT_WORKTREE_PATH/.python-version .python-version 2>/dev/null || true",
    "command -v direnv &> /dev/null && direnv allow",
    "bash scripts/bootstrap.sh"
  ]
}

The bootstrap.sh script does some checks and runs things like uv sync and npm ci.

This works really well and my new worktree command is now just wt setup <branch name> --trust.

Enter Claude

Last week Claude Code released full support for managing worktrees within Claude by passing the --worktree CLI flag. This works well but there is no setup hook (yet).

The solution I came up with was to use the SessionStart hook with a script that will check if it is running in a worktree. Here’s the pattern:

#!/usr/bin/env bash
# Run on SessionStart to set up the development environment.
# In a worktree: copies env files from the main worktree first.
# Always: installs dependencies via bootstrap.sh.

set -euo pipefail

ROOT_WORKTREE_PATH=$(git worktree list --porcelain | awk '/^worktree/{print $2; exit}')
CURRENT_PATH=$(git rev-parse --show-toplevel 2>/dev/null || pwd)

if [ "$CURRENT_PATH" != "$ROOT_WORKTREE_PATH" ]; then
    echo "Setting up worktree at $CURRENT_PATH"
    export ROOT_WORKTREE_PATH

    [ -f "$ROOT_WORKTREE_PATH/.env" ]   && [ ! -f ".env" ]   && cp "$ROOT_WORKTREE_PATH/.env"   .env
    [ -f "$ROOT_WORKTREE_PATH/.envrc" ] && [ ! -f ".envrc" ] && cp "$ROOT_WORKTREE_PATH/.envrc" .envrc
    command -v direnv &>/dev/null        && direnv allow
fi

"$ROOT_WORKTREE_PATH/scripts/bootstrap.sh"

echo "Setup complete."

Now update the Claude settings to call that hook:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/setup-env.sh"
          }
        ]
      }
    ]
  }
}

And we can use the same script in the worktrees.json file to avoid duplication:

{
  "setup-worktree": [
    "bash .claude/hooks/setup-env.sh"
  ]
}

Now both worktree-cli and claude -w get properly configured environments automatically.