agentleFS
tasklesstaskless/cli/.agents/skills/worktrees-pnpm/SKILL.md

worktrees-pnpm

Skill141 starsChanged 24 days agoMITView on GitHub

Create and work in git worktrees in this pnpm workspace. Use when creating a worktree, delegating to a background agent with worktree isolation, or when a worktree fails at commit time with a missing prettier/eslint/tsx binary. Covers why a worktree needs its own pnpm install, where worktrees live, and remediations for branch locks, moving a worktree, and failures that look like worktree problems but are not.

  • Installs packages
  • Commits and pushes

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 3 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Verify without waiting for an agent, by feeding a hook its payload directly:

    line 125
  2. Distinctive:

    A branch can live in only one worktree: If you need to edit a branch an agent worktree holds, you have three options, in order of …

    line 276
  3. Very specific:

    Never mv a worktree: Use git worktree move — it rewrites the .git pointers and carries node_modules along, so no reinstall is needed.

    line 295
  4. Distinctive:

    A git push is rejected as non-fast-forward: Check whether the remote branch was rebased independently (for example by GitHub's _Update …

    line 314

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Worktrees in a pnpm workspace

A git worktree is a second checkout of this repository sharing one .git. It gets its own working tree — and its own empty node_modules. That single fact causes every worktree problem in this repo.

The rule

git worktree add is not finished until pnpm install has run inside the new worktree.

git fetch origin +refs/heads/main:refs/remotes/origin/main   # explicit refspec; see the hook for why
git worktree add --no-track -b <branch> .agents/.worktrees/<name> origin/main   # or an existing <branch>, without -b
cd .agents/.worktrees/<name>
git rev-list --count HEAD..origin/main   # anything above 0: git rebase origin/main
pnpm install            # ← not optional, and after the rebase, which can change the lockfile

The base check applies whoever made the worktree. The WorktreeCreate hook fetches before branching, but a worktree made by hand or by another tool is cut from whatever its maker had locally. Stepping into one you did not create, run the fetch and the rev-list check above first, before reading code or installing.

Branch from origin/main, never the local main, which is only as fresh as the main checkout's last pull. A stale tree also carries stale conventions, including this one, so the check cannot wait until you have read the docs it would correct.

Worktrees go in .agents/.worktrees/, gitignored except for a tracked .gitkeep, so the directory exists in every clone. This is the location every agent harness shares, not a Claude Code one, so a worktree made by hand, by Claude Code, or by another tool lands in the same place. In Claude Code, the WorktreeCreate / WorktreeRemove hooks configured in .claude/settings.json (scripts in this skill's scripts/ directory) do the placement, which would otherwise nest worktrees at .claude/worktrees/<id>. A harness without those hooks follows the manual steps above, and the result is identical. ls .agents/.worktrees/ answers "what worktrees do I have?" at a glance.

Two older locations are still ignored, and new worktrees never go there. worktrees/ at the repo root was the previous standard, and .claude/worktrees/ is Claude Code's default. Both keep their ignore entries so that worktrees already living there do not surface as untracked files or get linted, until their work is done.

A sibling directory (../<repo>-worktrees/) is the obvious alternative and was tried first. It is genuinely better on one axis — nothing inside the repo, so no tool can walk into it — but its path depends on what the clone directory is named, which committed config cannot know. A teammate whose checkout is not named the same thing gets permission prompts for every file operation, silently and per person. .agents/.worktrees/ is the same path in every clone; the price is the ignore entries in the next section, which are a few lines you control.

A branch can only be checked out in one worktree at a time. If the branch you want is checked out in the primary tree, move that tree to another branch first.

Skipping the install leaves a checkout that looks fine and fails the moment you try to accomplish anything:

  • git commit fails or silently skips checks. Commits run lint-staged, which shells out to prettier and eslint from node_modules/.bin. Neither exists in a fresh worktree.
  • pnpm typecheck / lint / test fail — no typescript, no vitest, no vite.
  • pnpm openspec fails — the binary is not on PATH in this repo; it resolves through node_modules.
  • tsx scripts fail, including the CLI's own build and schema-generation scripts.

This has cost real time before: an agent working in a worktree without node_modules spent roughly an hour building workarounds for a missing prettier rather than running pnpm install.

There is no pnpm worktree command

pnpm does not provide worktree subcommands. Use git worktree — it is the correct tool. (pnpm's own repository has a worktree:new helper script, but that is a script in their repo, not a pnpm feature. Do not go looking for it here.)

pnpm's only contribution here is the install you owe the new worktree.

What the install actually costs

du reports node_modules at roughly 370 MB, but that is apparent size. The pnpm store lives on the same APFS volume, so package content is shared by copy-on-write rather than duplicated; the real incremental cost of another worktree is far smaller than the number suggests. Disk is not the reason to avoid a worktree.

enableGlobalVirtualStore is not used here, and that is settled. It would make installs a tree of symlinks into one shared store, but it is experimental, pnpm documents it as not working with ESM under hoisted dependencies, and this repo is ESM throughout. We gain nothing worth that risk. Do not re-propose it.

So a worktree install is a normal install. Budget for it; do not skip it.

Setup (per repo)

This skill ships the hook scripts it needs, in scripts/ beside this file. A repo that installs the skill gets them; wiring them up is two edits.

1. Register the hooks in the repo's committed .claude/settings.json. WorktreeCreate replaces the default placement logic entirely — it runs git worktree add itself and prints the path it made — and WorktreeRemove is its counterpart.

Every worktree starts from the remote's default branch as of the moment it is made. The hook fetches origin's default branch and branches from origin/<default>. It never uses the main checkout's HEAD, which is whatever that checkout was last pulled to: a worktree cut from it can start dozens of commits behind main, and nothing flags it until someone reads stale code as current. If the fetch fails (offline, no origin), creation fails with a message rather than falling back to a stale base. The new branch has no upstream; push it with an explicit refspec.

{
  "hooks": {
    "WorktreeCreate": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.agents/skills/worktrees-pnpm/scripts/worktree-create.sh\"",
            "statusMessage": "Creating worktree"
          }
        ]
      }
    ],
    "WorktreeRemove": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.agents/skills/worktrees-pnpm/scripts/worktree-remove.sh\""
          }
        ]
      }
    ]
  }
}

Two things that will waste your time if you get them wrong. The nested [{ "hooks": [...] }] shape is required — the flatter [{ type, command }] form shown on some documentation pages does not validate, and an invalid settings file disables every setting in it silently. And the scripts need jq on PATH; they read their payload from stdin.

Verify without waiting for an agent, by feeding a hook its payload directly:

printf '{"hook_event_name":"WorktreeCreate","cwd":"%s","worktree_id":"probe"}' "$PWD" \
  | .agents/skills/worktrees-pnpm/scripts/worktree-create.sh
# expect: <repo>/.agents/.worktrees/probe on stdout, git chatter on stderr, exit 0,
# and `git -C .agents/.worktrees/probe rev-parse HEAD` equal to `origin/main` just fetched

# The id field is not stable across callers, so .worktreeId and .name are read
# too — `.name` is what a background agent's payload actually carries.
printf '{"hook_event_name":"WorktreeCreate","cwd":"%s","name":"probe-name"}' "$PWD" \
  | .agents/skills/worktrees-pnpm/scripts/worktree-create.sh
# expect: <repo>/.agents/.worktrees/probe-name

printf '{"hook_event_name":"WorktreeRemove","worktree_path":"%s"}' "$PWD/.agents/.worktrees/probe" \
  | .agents/skills/worktrees-pnpm/scripts/worktree-remove.sh
# ...and the same for .agents/.worktrees/probe-name

2. Add the ignores described in the next section. Skipping this is the single most common way to end up debugging a lint failure in code you never wrote.

3. Refuse commits in the primary checkout. Instructions telling agents to stay out of the main checkout are not enough on their own, so a pre-commit hook makes it a hard stop. scripts/refuse-main-checkout.sh exits non-zero when git rev-parse --git-dir equals --git-common-dir, which is true only in the primary checkout, never in a linked worktree. It checks the checkout, not the branch, so a feature branch checked out in the primary tree is refused too. It skips when GITHUB_ACTIONS=true, because a runner commits from a plain clone (version PRs, review bots), and a plain clone is a primary checkout. It keys on the variable the runner sets itself rather than CI, which any shell can export as an unnamed bypass. A repo on another CI system adds that system's own marker. With Husky, run it first in .husky/pre-commit. This is the body taskless/cli, taskless/taskless and taskless/marketing share:

# GitHub Actions skips the dotagents install (postinstall checks CI), so the
# skill's guard may be absent there; the guard exempts Actions anyway.
if [ "${GITHUB_ACTIONS:-}" != "true" ]; then
  guard=.agents/skills/worktrees-pnpm/scripts/refuse-main-checkout.sh
  if [ ! -f "$guard" ]; then
    echo "pre-commit: $guard is missing. Run pnpm install (or pnpm dotagents install) first." >&2
    exit 1
  fi
  sh "$guard" || exit 1
fi
pnpm exec lint-staged

The || exit 1 is load-bearing outside Husky. Husky 9 runs a hook as sh -e, so a bare sh "$guard" line happens to stop there, but the same two lines as a raw git hook carry on into lint-staged, and its passing exit lets the refused commit through. The missing-file check turns a bare sh error into one that says what to run. It sits inside the GITHUB_ACTIONS test because a repo that installs this skill through dotagents skips that install under CI, so on a runner the guard may not exist, and the check would otherwise block every automated commit.

Without Husky, call it from .git/hooks/pre-commit, or from whatever hook manager the repo uses. A person who really means to commit in the primary checkout runs git commit --no-verify. An agent never gets past it, whether by passing --no-verify, setting GITHUB_ACTIONS, or any other means; a refusal means it is in the wrong checkout, so it moves to a worktree.

Every root-level tool must ignore the worktrees directory

This is the one real cost of keeping worktrees in the repo, and it is not optional. A worktree is a complete second checkout. Anything that walks the tree from the root will walk into it — linting, formatting, or type-checking another branch's code, and failing on whatever an agent has half-written. The symptom is bewildering: your tooling fails on code you never wrote.

Placing worktrees outside the repo avoids this, but the path then depends on what the clone directory is named, so committed config cannot reference it portably. In-repo plus ignores is the trade: three one-line entries you control, instead of a path that silently differs per teammate.

Add the worktrees directory to every tool that walks the tree from the root. In this repo that is the current location plus the two older ones:

# .gitignore
.agents/.worktrees/*
!.agents/.worktrees/.gitkeep
worktrees/
.claude/worktrees/
# .prettierignore
.agents/.worktrees/
worktrees/
.claude/worktrees/
// eslint.config.js (flat config)
export default [
  { ignores: [".agents/.worktrees/", "worktrees/", ".claude/worktrees/"] },
];

Other ecosystems, same idea:

// .eslintrc.json (legacy)
{ "ignorePatterns": [".agents/.worktrees/"] }
// biome.json
{ "files": { "ignore": [".agents/.worktrees/"] } }
# pyproject.toml — ruff / black
[tool.ruff]
exclude = [".agents/.worktrees"]
[tool.black]
extend-exclude = "\\.agents/\\.worktrees"
// tsconfig.json — only if the root config globs sources itself
{ "exclude": [".agents/.worktrees"] }

Also check anything else that globs from the root: test runners (vitest/jest exclude), bundlers, coverage tools, .dockerignore, and workspace globs in pnpm-workspace.yaml, package.json workspaces, or a Cargo/Go workspace file.

Do not assume which tools are affected — measure. In this repo, pnpm workspaces and tsc turned out not to need an entry (the workspace glob is root-anchored, and typecheck runs per-package through turbo), while prettier did. That was invisible until worktrees moved out of a dot-directory, because prettier's globs skip dotfiles. They are back in one now, and the entry stays anyway: being skipped by luck of a leading dot is not the same as being excluded. Your repo's answer will differ.

The check that settles it, run with a worktree actually present:

git worktree add .agents/.worktrees/probe -b throwaway-probe
touch .agents/.worktrees/probe/UNFORMATTED.md

git status --porcelain | grep -c '^?? .agents'                     # expect 0
pnpm exec prettier --list-different . | grep -c .worktrees/probe   # expect 0
pnpm exec eslint --debug . 2>&1 | grep -c .worktrees/probe         # expect 0
# ...and the same shape for every other root-level tool

git worktree remove .agents/.worktrees/probe && git branch -D throwaway-probe

Any non-zero count is a tool that still needs an ignore entry.

Gotchas and remediations

Each of these was hit for real in this repo.

A branch can live in only one worktree. If you need to edit a branch an agent worktree holds, you have three options, in order of preference: hand the edit to the agent that owns it; work directly inside that worktree, but only if the agent is idle — never while it is running; or remove the worktree if it has no work worth keeping.

An idle agent's worktree still holds its branch lock. A finished or stopped agent leaves the worktree registered, so its branch stays unavailable. Check before reclaiming it:

git -C <worktree> status --short                        # uncommitted work?
git -C <worktree> log --oneline origin/<branch>..HEAD   # unpushed commits?
git worktree remove <worktree>                          # only when both are empty

git worktree list shows a worktree-<id> branch you did not create. That is the placeholder branch made at creation time; an agent checks out the branch it actually needs afterward, so the placeholder is not where the work is. Look at the branch the agent reports, not the one in the listing.

Never mv a worktree. Use git worktree move — it rewrites the .git pointers and carries node_modules along, so no reinstall is needed. Moving the directory by hand leaves the worktree pointing at a path that no longer exists.

A worktree survives between agent runs. Resuming an agent reuses its worktree and its node_modules, so a resumed agent does not pay the install again. Tell it to cd $PWD fresh rather than trusting a path it cached in an earlier run — the worktree may have been moved.

Problems that look like worktree problems and are not

Worth knowing, because misdiagnosing these wastes real time:

  • --force-with-lease fails with stale info on every branch. That is a shallow or single-branch clone, not a worktree issue — there is no remote-tracking ref to lease against. See the shallow-clone entry in AGENTS.md. The same cause breaks git push -u and makes gh pr create demand an explicit --head.
  • Several branches suddenly need rebasing and force-pushing at once. That is main moving under an open stack, which happens whenever a PR merges. It is a stacking-cadence concern; see the stacked-PR guidance in AGENTS.md.
  • A git push is rejected as non-fast-forward. Check whether the remote branch was rebased independently (for example by GitHub's Update branch) before assuming your local history is wrong. If the remote already contains your commits under new SHAs, replay only what is missing with git rebase --onto origin/<branch> <old-base> <branch> rather than force-pushing over it.

Delegating to a background agent with worktree isolation

  • Never point the agent at the main repo path. Given an absolute path to the primary checkout, an agent will cd there and run its git commands and edits in your working tree — creating and checking out branches under you, silently moving your session off its own branch. Tell the agent to work in its assigned worktree ($PWD) and pass only relative paths plus GitHub identifiers (owner/repo).
  • Tell it to run pnpm install first, or it will hit the failures above at commit time, which is the worst moment to discover them.
  • If it needs a tool and cannot install, it can invoke the main checkout's binaries against the worktree's files — but prefer installing.
  • If the agent stalls in a degraded shell (every command taking minutes), stop it (TaskStop) and finish the work directly rather than waiting it out. Check its scratchpad first for artifacts it already produced.

Cleaning up

git worktree remove .agents/.worktrees/<name>   # add --force if it has uncommitted changes
git worktree list                                 # confirm

Removing the directory by hand leaves a stale registration; git worktree prune clears it.

To relocate an existing worktree, use git worktree move <from> <to> — it rewrites the .git pointers, and node_modules comes along, so no reinstall is needed. Moving the directory yourself leaves the worktree pointing at a path that no longer exists.

Recovery: the main checkout got switched onto an agent's branch

Your work is safe as long as it was pushed — confirm origin/<branch> and the PR head SHA still match your last commit. Then:

git worktree remove --force <path>
git branch -D <stray-branch>
git checkout <your-branch>

When a worktree is worth it

Worktrees cost a pnpm install and a little wall-clock. They earn it when you need two branches checked out at once — running a long test suite on one branch while editing another, or letting a background agent work without disturbing your tree.

For sequential work on several branches, plain git checkout in one checkout is cheaper and has none of these failure modes.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.