Skip to content

Skill generator: files occasionally land in main checkout instead of worktree #53

@leggetter

Description

@leggetter

Summary

When running ./scripts/generate-skills.sh generate with multiple providers in parallel, the spawned claude subprocess for a provider occasionally writes its generated skills/<provider>-webhooks/ tree into the main repository checkout instead of the per-provider worktree at .worktrees/<provider>, despite the generator passing the worktree path as cwd to execa. The result is that the worktree's commit is empty (or the generator reports success on a tree that doesn't contain the files), and the new skill ends up untracked in the main checkout.

Observed during

A batched run of 12 providers with --parallel 3 --model claude-opus-4-7:

IS_SANDBOX=1 ./scripts/generate-skills.sh generate \
  claude-managed-agents discord gemini huggingface hubspot intercom \
  linear mailgun notion paypal slack twilio \
  --config providers.yaml --parallel 3 --model claude-opus-4-7

2 of 12 providers exhibited the issue (~17%):

  • claude-managed-agents — files generated into the main checkout's skills/claude-managed-agents-webhooks/; the worktree at .worktrees/claude-managed-agents was empty. Recovered manually by git mv-ing the tree into the worktree, committing, and pushing.
  • huggingface — same pattern; same manual recovery.

The other 10 providers landed in their worktrees correctly. All three subprocesses were running in parallel, so it isn't a race against a single shared file lock — it appears to be a path-resolution issue inside the claude CLI itself when its CWD is a git worktree.

Why this matters

  • Silent partial failures. The generator's git diff --stat check (or whatever it uses to confirm new files exist) reports 0 files changed for the affected worktree, but the subprocess may still report success or have iterated on test results from the other checkout. The acceptance thresholds can pass against files that aren't on the branch being committed.
  • State leaks into the main checkout. The unrelated files appear as untracked in whatever branch the operator has checked out — easy to mis-stage on a subsequent commit.
  • Hard to reproduce. It's intermittent and only shows up when running multiple worktree-based subprocesses in parallel.

Suspected root cause

The claude CLI does some path resolution against its CWD on startup. When the CWD is a git worktree (.worktrees/<provider>), it appears to occasionally resolve back to the parent repository's working tree — possibly via git rev-parse --show-toplevel (which returns the main worktree's path, not the linked worktree's path, unless --git-common-dir/--show-prefix is used appropriately).

A quick mitigation worth testing: have scripts/skill-generator/lib/cli-adapters/claude.ts pass --add-dir <workingDir> and/or set an explicit project-root hint in the prompt so the CLI cannot fall back to the parent tree.

Reproduction

  1. Check out this repository on a system where you can run the generator (IS_SANDBOX=1 if running as root — see AGENTS.md).
  2. Run the generator with at least 3 providers in parallel against fresh worktrees.
  3. After completion, inspect git status in the main checkout — if any skills/<provider>-webhooks/ directories appear as untracked, the bug fired. Check the corresponding worktree at .worktrees/<provider> — it should be empty.

Not deterministic, but at ~17% per run with 3 parallel subprocesses the failure rate is high enough to investigate.

Workaround for now

When operating the generator in batch, after each provider's "Completed successfully!" event:

# Sanity check
ls .worktrees/<provider>/skills/<provider>-webhooks/
git -C . status --short  # should be empty in the main checkout

If files leaked to the main checkout, git mv them into the worktree, then commit + push from the worktree.

Suggested fix candidates

  1. Inspect what path the claude CLI resolves when CWD is a worktree — log it from a wrapper and confirm whether git rev-parse --show-toplevel returns the worktree path or the main path in that context.
  2. In claude.ts, pass --add-dir <workingDir> explicitly so the agent has a clear scoped directory regardless of what the CLI auto-detects.
  3. Add a post-generation invariant in the generator: assert that git -C <workingDir> diff --stat HEAD reports new files in skills/<provider>-webhooks/. Fail the provider (instead of returning success) if the worktree is empty.
  4. Optionally, before committing each worktree, run git -C <mainRepo> status --porcelain skills/<provider>-webhooks/ and refuse to proceed if a leak is detected — surfaces the bug instead of swallowing it.

Found while running the new-provider generation batch that opened PRs #41#52.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions