Skip to content

Instantly share code, notes, and snippets.

@craigsc
Created February 11, 2026 23:12
Show Gist options
  • Select an option

  • Save craigsc/46979018ee28788602d3452fb08ef814 to your computer and use it in GitHub Desktop.

Select an option

Save craigsc/46979018ee28788602d3452fb08ef814 to your computer and use it in GitHub Desktop.
Claude Tree Helper Commands
# ct — Claude worktree helpers for parallel work
# https://gist.github.com/craigsc/46979018ee28788602d3452fb08ef814
#
# Lightweight zsh functions for managing git worktrees, designed for
# running multiple Claude Code sessions in parallel on the same repo.
#
# Each worktree gets its own branch and directory, with configurable
# symlinks so you don't have to reinstall dependencies every time.
#
# Functions:
# ct <name> — Create a worktree, symlink shared files, launch Claude
# ctcd <name> — cd into an existing worktree
# ctrm [name] — Remove a worktree and its branch (no args = current worktree)
# ctls — List all worktrees
# ctiso — Replace symlinked node_modules with a real install
#
# Names can use spaces: `ct my feature` becomes branch `my-feature`
# and directory `<repo>-my-feature` alongside your main repo.
# ── Configuration ─────────────────────────────────────────────────────
# Point this at your main (non-worktree) repo checkout:
CT_REPO_ROOT=~/Projects/pastmaps
# Files/directories to symlink from the main repo into each new worktree.
# Remove or add entries to match your project:
CT_SYMLINKS=(node_modules .dev.vars)
# Command to run after creating a worktree (set to "" to disable):
CT_AUTO_COMMAND="claude"
# ──────────────────────────────────────────────────────────────────────
# Derived — no need to edit these
_ct_repo_name="${CT_REPO_ROOT:t}" # e.g. "pastmaps"
_ct_parent_dir="${CT_REPO_ROOT:h}" # e.g. ~/Projects
# Create a worktree with a new branch and launch Claude
# Usage: ct feature-name OR ct feature name with spaces
ct() {
if [ -z "$1" ]; then
echo "Usage: ct <name> (spaces are auto-hyphenated)"
return 1
fi
local feature="${(j:-:)@}"
local worktree_dir="$_ct_parent_dir/${_ct_repo_name}-${feature}"
git -C "$CT_REPO_ROOT" worktree add "$worktree_dir" -b "$feature" || return 1
cd "$worktree_dir"
# Symlink shared files/directories from the main repo
for item in $CT_SYMLINKS; do
if [ -e "$CT_REPO_ROOT/$item" ]; then
ln -sf "$CT_REPO_ROOT/$item" "$item"
fi
done
echo "Worktree ready: $worktree_dir"
[ -n "$CT_AUTO_COMMAND" ] && eval "$CT_AUTO_COMMAND"
}
# cd into an existing worktree
# Usage: ctcd feature-name OR ctcd feature name with spaces
ctcd() {
if [ -z "$1" ]; then
echo "Usage: ctcd <name> (spaces are auto-hyphenated)"
return 1
fi
local feature="${(j:-:)@}"
local worktree_dir="$_ct_parent_dir/${_ct_repo_name}-${feature}"
if [ ! -d "$worktree_dir" ]; then
echo "Worktree not found: $worktree_dir"
echo "Run 'ctls' to see available worktrees."
return 1
fi
cd "$worktree_dir"
[ -f .dev.vars ] && source .dev.vars
}
# Remove a worktree and its branch
# Usage: ctrm feature-name OR ctrm (no args = current worktree)
ctrm() {
local feature worktree_dir
if [ -z "$1" ]; then
# No argument — remove current worktree if we're in one
if [[ "$PWD" == "$_ct_parent_dir/${_ct_repo_name}"-* ]]; then
worktree_dir="$PWD"
feature="${worktree_dir:t}" # basename
feature="${feature#${_ct_repo_name}-}" # strip repo prefix
cd "$CT_REPO_ROOT"
else
echo "Usage: ctrm <name> (or run with no args from inside a worktree)"
return 1
fi
else
feature="${(j:-:)@}"
worktree_dir="$_ct_parent_dir/${_ct_repo_name}-${feature}"
fi
if [ ! -d "$worktree_dir" ]; then
echo "Worktree not found: $worktree_dir"
return 1
fi
git -C "$CT_REPO_ROOT" worktree remove "$worktree_dir" && \
git -C "$CT_REPO_ROOT" branch -d "$feature" 2>/dev/null
}
# List all worktrees (excluding the main repo)
ctls() {
git -C "$CT_REPO_ROOT" worktree list | tail -n +2
}
# Isolate node_modules — replace the symlink with a real install
# (useful when you need to modify dependencies in a worktree)
ctiso() {
if [ -L "node_modules" ]; then
echo "Removing symlink and running npm install..."
rm node_modules
npm install
else
echo "node_modules is not a symlink — already isolated."
fi
}
@woodward54
Copy link

This is great! super useful

Slightly updated version to work with branches with slashes. i.e. ctrm dw/remove-err-msg-2, also added a flag to open with cursor instead of claude

Also I use pnpm so needed to run pnpm i to link the deps

# ct — Claude worktree helpers for parallel work
# https://gist.github.com/craigsc/46979018ee28788602d3452fb08ef814
#
# Lightweight zsh functions for managing git worktrees, designed for
# running multiple Claude Code sessions in parallel on the same repo.
#
# Each worktree gets its own branch and directory, with configurable
# symlinks so you don't have to reinstall dependencies every time.
#
# Functions:
#   ct <name>           — Create a worktree, symlink shared files, launch Claude
#   ct --cursor <name>  — Same as above, but open in Cursor instead of CLI Claude
#   ctcd <name>   — cd into an existing worktree
#   ctrm [name]   — Remove a worktree and its branch (no args = current worktree)
#   ctls          — List all worktrees
#   ctiso         — Replace symlinked node_modules with a real install
#
# Names can use spaces: `ct my feature` becomes branch `my-feature`
# and directory `<repo>-my-feature` alongside your main repo.

# ── Configuration ─────────────────────────────────────────────────────
# Point this at your main (non-worktree) repo checkout:
CT_REPO_ROOT=~/dev/merlin

# Files/directories to symlink from the main repo into each new worktree.
# Remove or add entries to match your project:
CT_SYMLINKS=()

# Command to run after creating a worktree (set to "" to disable):
CT_AUTO_COMMAND="claude"
# ──────────────────────────────────────────────────────────────────────

# Derived — no need to edit these
_ct_repo_name="${CT_REPO_ROOT:t}"   # e.g. "pastmaps"
_ct_parent_dir="${CT_REPO_ROOT:h}"  # e.g. ~/Projects

# Create a worktree with a new branch and launch Claude
# Usage: ct [--cursor] feature-name OR ct feature name with spaces
ct() {
  local use_cursor=false
  if [[ "$1" == "--cursor" ]]; then
    use_cursor=true
    shift
  fi

  if [ -z "$1" ]; then
    echo "Usage: ct [--cursor] <name>  (spaces are auto-hyphenated)"
    return 1
  fi

  local feature="${(j:-:)@}"
  local dir_name="${feature//\//-}"
  local worktree_dir="$_ct_parent_dir/${_ct_repo_name}-${dir_name}"

  git -C "$CT_REPO_ROOT" worktree add "$worktree_dir" -b "$feature" || return 1

  cd "$worktree_dir"

  # Symlink shared files/directories from the main repo
  for item in $CT_SYMLINKS; do
    if [ -e "$CT_REPO_ROOT/$item" ]; then
      ln -sf "$CT_REPO_ROOT/$item" "$item"
    fi
  done

  # Install dependencies
  cd frontend
  pnpm i
  cd ..

  echo "Worktree ready: $worktree_dir"

  if $use_cursor; then
    # Open in Cursor and run Claude Code in the integrated terminal
    cursor "$worktree_dir"
  elif [ -n "$CT_AUTO_COMMAND" ]; then
    eval "$CT_AUTO_COMMAND"
  fi
}

# cd into an existing worktree
# Usage: ctcd feature-name OR ctcd feature name with spaces
ctcd() {
  if [ -z "$1" ]; then
    echo "Usage: ctcd <name>  (spaces are auto-hyphenated)"
    return 1
  fi

  local feature="${(j:-:)@}"
  local dir_name="${feature//\//-}"
  local worktree_dir="$_ct_parent_dir/${_ct_repo_name}-${dir_name}"

  if [ ! -d "$worktree_dir" ]; then
    echo "Worktree not found: $worktree_dir"
    echo "Run 'ctls' to see available worktrees."
    return 1
  fi

  cd "$worktree_dir"
  [ -f .dev.vars ] && source .dev.vars
}

# Remove a worktree and its branch
# Usage: ctrm feature-name OR ctrm (no args = current worktree)
ctrm() {
  local feature worktree_dir

  if [ -z "$1" ]; then
    # No argument — remove current worktree if we're in one
    if [[ "$PWD" == "$_ct_parent_dir/${_ct_repo_name}"-* ]]; then
      worktree_dir="$PWD"
      # Get the actual branch name from git (reliable even with / in names)
      feature="$(git -C "$PWD" rev-parse --abbrev-ref HEAD)"
      cd "$CT_REPO_ROOT"
    else
      echo "Usage: ctrm <name>  (or run with no args from inside a worktree)"
      return 1
    fi
  else
    feature="${(j:-:)@}"
    local dir_name="${feature//\//-}"
    worktree_dir="$_ct_parent_dir/${_ct_repo_name}-${dir_name}"
  fi

  if [ ! -d "$worktree_dir" ]; then
    echo "Worktree not found: $worktree_dir"
    return 1
  fi

  git -C "$CT_REPO_ROOT" worktree remove "$worktree_dir" && \
    git -C "$CT_REPO_ROOT" branch -d "$feature" 2>/dev/null
}

# List all worktrees (excluding the main repo)
ctls() {
  git -C "$CT_REPO_ROOT" worktree list | tail -n +2
}

# Isolate node_modules — replace the symlink with a real install
# (useful when you need to modify dependencies in a worktree)
ctiso() {
  if [ -L "node_modules" ]; then
    echo "Removing symlink and running npm install..."
    rm node_modules
    npm install
  else
    echo "node_modules is not a symlink — already isolated."
  fi
}

@craigsc
Copy link
Author

craigsc commented Feb 13, 2026

hey this is awesome! you may want to scope out https://github.com/craigsc/cmux too - I just spent some time iterating on the base scripts here and rolling them into a single worktree manager

it's quite a bit more robust and auto-generated per-project setup hooks that would have made this work out of the box with pnpm

i'd gladly accept PRs on it as well if you find this useful for your own work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment