Skip to content

chore(hooks): install dependencies at session start and worktree entry - #118

Merged
aliasunder merged 4 commits into
mainfrom
chore/session-dep-hooks
Sep 30, 2026
Merged

aliasunder merged 4 commits into
mainfrom
chore/session-dep-hooks

Conversation

@aliasunder

@aliasunder aliasunder commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Problem

Fresh checkouts start without node_modules. A cloud (claude.ai/code) session clones the repo after the environment's setup script has already run, and a new git worktree never inherits the main checkout's node_modules. Every agent session in one of those checkouts has to notice the missing dependencies and install them before tests, lint, or build can run.

Change

  • .claude/settings.json (new, committed) registers install-deps.sh on SessionStart (startup|resume) and on PostToolUse for EnterWorktree, with a 600s timeout.
  • .claude/hooks/install-deps.sh (new, committed):
    • Resolves the active checkout from the hook payload's cwd, which follows the session into a worktree. $CLAUDE_PROJECT_DIR stays at the project root, so it is only the fallback.
    • Exits immediately when node_modules exists, unless an install was interrupted or the hook's own last install used a different package-lock.json. An install the hook did not make is never replaced.
    • Serializes installs with a flock on a lock file. npm ci inherits the locked descriptor, so an orphaned install keeps the lock until it finishes, and the kernel releases it once every holder exits. No stale lock is ever left behind. A session that finds the lock held waits up to 480s, then re-checks whether the other session already installed. Perl provides the flock call, because macOS lacks flock(1) and Linux lacks lockf(1). When Perl is missing or the lock call fails for any reason other than the 480s timeout, the hook installs unlocked and logs why.
    • Keeps its marker, stamp, and lock file in the checkout's git directory (git rev-parse --absolute-git-dir). That directory is per-worktree and never tracked, so no checkout shows them as untracked files.
    • An incomplete-install marker makes an interrupted npm ci retry next session. The marker holds the lockfile hash its install used. Under the lock, a tree that passes npm ls with a matching hash is accepted instead of reinstalled.
    • Otherwise loads nvm ($HOME/.nvm, or /root/.nvm for cloud setup), switches to the .nvmrc Node when it is already installed, and runs npm ci with all output on stderr.
  • .gitignore: .claude/* with re-includes for exactly these two files. settings.local.json, worktrees/, and local hook files stay ignored, including in cloud clones without a global gitignore.
  • AGENTS.md: the structure tree lists the new files.

Verification

  • git check-ignore -v --no-index: settings.local.json, worktrees/, and an extra hook file are ignored. settings.json and install-deps.sh are re-included.

  • Each case below ran against a real checkout:

    Case Result
    Fresh worktree, no node_modules Real npm ci (171 packages), stamp written, marker removed
    Installed, unstamped or stamped Exits 0, "nothing to do"
    Marker with the current lockfile hash Cleared under the lock after npm ls passes
    Marker with an older lockfile hash Real reinstall instead of recovery
    Lock held by an orphan (a background process holding only the inherited descriptor, after the locker exited) Waits about 4s for the orphan to exit, then recovers
    Two hooks started together on a checkout with no node_modules One npm ci. The other waits, then reports the install finished
    Same, with the lock mutated out Both run npm ci into the same node_modules, confirming the lock is what serializes them
    Bare git repo with no .claude/, .gitignore, or package.json npm ci fails, the marker lands in .git/, exit 0, git status empty
  • git status --untracked-files=all in the worktree shows no hook state after the runs.

  • The PostToolUse registration on EnterWorktree fires in practice. Session transcripts from another repo using the same registration show it running npm ci in newly entered worktrees.

  • shellcheck is clean. npm run lint and npm test (800 tests) pass.

🤖 Generated with Claude Code

Fresh cloud clones and fresh git worktrees start without node_modules. A committed SessionStart hook and a PostToolUse hook on EnterWorktree run install-deps.sh, which loads nvm and runs npm ci only when the checkout has no node_modules, or when the hook's own last install used a different package-lock.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread .claude/hooks/install-deps.sh Outdated
Comment thread .claude/hooks/install-deps.sh Outdated
Comment thread .claude/hooks/install-deps.sh Outdated
@umm-actually

umm-actually Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

umm-actually re-reviewed at 944d3c5

1 new finding(s) posted (8 tracked finding(s) across all runs).


umm-actually · deepseek/deepseek-v4.1-flash

- Re-read the lock owner after the takeover claim is held, so a session never removes a lock another session just took over.
- Run the interrupted-install recovery only while holding the lock, so a half-written tree from a still-running npm ci is never stamped as complete.
- Re-read an empty lock pid after one second, so a lock whose pid write is still in flight is not treated as abandoned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread .claude/hooks/install-deps.sh Outdated
Comment thread .claude/hooks/install-deps.sh Outdated
Comment thread .claude/hooks/install-deps.sh
…lockfile

- The marker, stamp, and lock move from .claude/ to the checkout's git directory, which is per-worktree and never tracked, so a checkout whose .gitignore predates the hook never shows them as untracked files.
- The marker now holds the lockfile hash its install used, and interrupted-install recovery runs only when that hash matches the current lockfile, so a tree built from an older lockfile is reinstalled instead of stamped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread .claude/hooks/install-deps.sh Outdated
…aling mkdir locks

The pid-file lock needed a takeover protocol to recover from a dead owner, and every takeover built from rm and mkdir left a check-then-act gap where two sessions could both install. A flock on a lock file in the git directory replaces it. The kernel releases the lock when every holder exits, so there is nothing to steal. npm ci inherits the locked descriptor, so an orphaned install keeps the lock until it finishes. Perl provides the flock call on both macOS and Linux; without perl the hook installs unlocked and says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread .claude/hooks/install-deps.sh
@aliasunder
aliasunder merged commit 6b3610f into main Sep 30, 2026
9 checks passed
@aliasunder
aliasunder deleted the chore/session-dep-hooks branch September 30, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant