-
Notifications
You must be signed in to change notification settings - Fork 0
chore: keep eslint out of agent worktrees #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # WorktreeCreate hook — place worktrees BESIDE the repo, not inside it. | ||
| # | ||
| # Default Claude Code behavior creates worktrees at <repo>/.claude/worktrees/<id>. | ||
| # A worktree is a complete second checkout, so nesting it inside the repo means | ||
| # every tool that walks the tree from the root walks into it. We hit that: a root | ||
| # `eslint .` traversed 2983 files across two agent worktrees and failed on code an | ||
| # agent had half-written. An ignore rule patches one tool; placing worktrees | ||
| # outside the repo makes the whole class of problem impossible. | ||
| # | ||
| # Layout: /path/to/<repo> -> /path/to/<repo>-worktrees/<worktree_id> | ||
| # | ||
| # Contract (docs: code.claude.com/docs/en/hooks): | ||
| # stdin - JSON with .worktree_id (and .base_path, .cwd, .session_id, ...) | ||
| # stdout - the absolute path of the created worktree, plain text, REQUIRED | ||
| # exit - non-zero, or zero with empty stdout, fails worktree creation | ||
| # This hook replaces the default logic entirely, so it must run `git worktree add`. | ||
| set -euo pipefail | ||
|
|
||
| payload=$(cat) | ||
| worktree_id=$(printf '%s' "$payload" | jq -r '.worktree_id // empty') | ||
| if [ -z "$worktree_id" ]; then | ||
| echo "WorktreeCreate: no .worktree_id on stdin" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # The id reaches both a filesystem path and a git ref, so constrain it rather | ||
| # than trusting its origin: `..` would escape the sibling directory, a leading | ||
| # `-` could be read as a flag, and ref-invalid characters would fail the | ||
| # `worktree add` further down with a much less obvious error. The harness only | ||
| # ever sends simple slugs; this makes that assumption explicit and cheap to keep. | ||
| if ! [[ "$worktree_id" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || [[ "$worktree_id" == *..* ]]; then | ||
| echo "WorktreeCreate: refusing unsafe worktree_id '$worktree_id'" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Resolve against the repo this hook was invoked for, not $PWD. | ||
| cwd=$(printf '%s' "$payload" | jq -r '.cwd // empty') | ||
| repo_root=$(git -C "${cwd:-$PWD}" rev-parse --show-toplevel) | ||
| worktree_dir="$(dirname "$repo_root")/$(basename "$repo_root")-worktrees/$worktree_id" | ||
|
|
||
| # Idempotent, but only for a real worktree. A bare directory test would hand back | ||
| # a path that git knows nothing about — left by a partial cleanup, an interrupted | ||
| # run, or a stray mkdir — and the caller would treat creation as successful. | ||
| if git -C "$repo_root" worktree list --porcelain | grep -qxF "worktree $worktree_dir"; then | ||
| echo "$worktree_dir" | ||
| exit 0 | ||
| fi | ||
|
|
||
| # A directory that is NOT a registered worktree is an unsafe place to add one: | ||
| # `git worktree add` would fail on a non-empty path anyway, so say why plainly. | ||
| if [ -e "$worktree_dir" ]; then | ||
| echo "WorktreeCreate: $worktree_dir exists but is not a registered worktree" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| mkdir -p "$(dirname "$worktree_dir")" | ||
|
|
||
| # Mirror the default: a fresh branch per worktree, based on current HEAD. The | ||
| # agent checks out whatever branch it actually needs once inside. -B rather than | ||
| # -b so a leftover branch from a removed worktree does not wedge creation. | ||
| # All git chatter goes to stderr; stdout carries the path and nothing else. | ||
| git -C "$repo_root" worktree add -B "worktree-$worktree_id" "$worktree_dir" >&2 | ||
|
|
||
| echo "$worktree_dir" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # WorktreeRemove hook — counterpart to worktree-create.sh. | ||
| # | ||
| # Fires when a session ends, a subagent finishes, or a background session is | ||
| # deleted. Because WorktreeCreate replaced the default creation logic, this must | ||
| # do the corresponding removal. | ||
| # | ||
| # Contract (docs: code.claude.com/docs/en/hooks): | ||
| # stdin - JSON with .worktree_path | ||
| # stdout - ignored; this event is side-effect only | ||
| # exit - failures are logged in debug mode only and never surface to the user | ||
| # | ||
| # Because failures are invisible, this errs toward leaving things in place rather | ||
| # than deleting aggressively: uncommitted work in a worktree is real work, and a | ||
| # stale worktree is a cheap, visible problem (`git worktree list`) whereas | ||
| # silently discarded changes are not. | ||
| set -euo pipefail | ||
|
|
||
| payload=$(cat) | ||
| worktree_path=$(printf '%s' "$payload" | jq -r '.worktree_path // empty') | ||
| [ -n "$worktree_path" ] || exit 0 | ||
| [ -d "$worktree_path" ] || exit 0 | ||
|
|
||
| repo_root=$(git -C "$worktree_path" rev-parse --path-format=absolute --git-common-dir 2>/dev/null | xargs dirname) || exit 0 | ||
|
|
||
| # Refuse to discard uncommitted changes. `git worktree remove` without --force | ||
| # already refuses, but check explicitly so the reason is greppable in debug logs. | ||
| if [ -n "$(git -C "$worktree_path" status --porcelain 2>/dev/null)" ]; then | ||
| echo "WorktreeRemove: $worktree_path has uncommitted changes; leaving it in place" >&2 | ||
| exit 0 | ||
| fi | ||
|
|
||
| git -C "$repo_root" worktree remove "$worktree_path" >&2 || { | ||
| echo "WorktreeRemove: could not remove $worktree_path; leaving it for manual cleanup" >&2 | ||
| exit 0 | ||
| } | ||
|
|
||
| # Drop the per-worktree branch this hook's counterpart created, but only if it is | ||
| # fully merged — `-d` (not `-D`) so an unmerged branch, which may be the only | ||
| # reference to real work, is left alone. Best-effort: both the "Deleted branch" | ||
| # notice and any refusal go to stderr for debug logs, and neither is fatal. | ||
| branch="worktree-$(basename "$worktree_path")" | ||
| git -C "$repo_root" branch -d "$branch" >&2 || true | ||
|
|
||
| # Remove the parent directory only when it is empty, so the sibling directory | ||
| # does not linger once the last worktree is gone. | ||
| rmdir "$(dirname "$worktree_path")" 2>/dev/null || true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.