Skip to content

Instantly share code, notes, and snippets.

@B4nan
Created August 11, 2026 15:09
Show Gist options
  • Select an option

  • Save B4nan/29db31f4abe23cd6e486a3388bc54e2e to your computer and use it in GitHub Desktop.

Select an option

Save B4nan/29db31f4abe23cd6e486a3388bc54e2e to your computer and use it in GitHub Desktop.
Installing deps on SessionStart hook for worktrees
#!/usr/bin/env bash
# SessionStart hook — keep node deps in sync with the lockfile. Covers both the fresh
# worktree (node_modules absent) and the REUSED worktree whose node_modules was installed
# against an older lockfile (real case: apify-sdk-js Aug 2026, June install vs a same-day
# crawlee bump → phantom type errors). NON-BLOCKING: the install is spawned fully detached
# and the hook returns immediately, so session start is never held up.
[ -f package.json ] || exit 0 # only node projects
# Freshness check: a stamp with the lockfile hash is written after each successful install.
# Stamp matches → node_modules is provably in sync → skip (zero cost). Anything else →
# reinstall. The stamp (not `ni --frozen` unconditionally) is what makes "always install"
# safe generically: for npm projects `ni --frozen` = `npm ci`, which nukes node_modules and
# does a full reinstall every time.
LOCKHASH=""
for f in pnpm-lock.yaml yarn.lock package-lock.json npm-shrinkwrap.json bun.lock bun.lockb; do
[ -f "$f" ] && LOCKHASH="$LOCKHASH$(shasum "$f")"
done
STAMP="node_modules/.claude-deps-lockhash"
if [ -d node_modules ]; then
# No lockfile → nothing to compare against; keep the legacy "present → skip" behavior.
[ -z "$LOCKHASH" ] && exit 0
[ "$(cat "$STAMP" 2>/dev/null)" = "$LOCKHASH" ] && exit 0
fi
# Hooks run in a non-interactive shell. If node wasn't inherited on PATH, fall back to
# initializing the version manager so node/npm resolve. fnm is only a PATH fallback here.
if ! command -v node >/dev/null 2>&1; then
export PATH="/opt/homebrew/bin:$PATH"
command -v fnm >/dev/null 2>&1 && eval "$(fnm env 2>/dev/null)"
fi
LOG="${TMPDIR:-/tmp}/claude-deps-$(printf '%s' "$PWD" | shasum | cut -c1-12).log"
printf '[%s] %s\n' "$(date)" "$PWD" > "$LOG"
# Detach fully: redirect every fd off the hook's pipe (and </dev/null) so the parent hook
# isn't kept alive waiting on the child — that is what makes this non-blocking. PATH and the
# stamp vars are exported so the child inherits them.
# - Ensure `ni` is installed (it lives in the per-node-version global npm bin, so it can
# disappear after a node version switch); install it via npm if absent.
# - Then run `ni --frozen` (→ install with a frozen lockfile): a worktree only tests branch
# source, never edits deps, so this is faster (skips lockfile resolution) and fails loudly
# if package.json and the lockfile are out of sync instead of silently installing the wrong tree.
# - On success, write the lockfile-hash stamp so future in-sync sessions skip instantly.
export LOCKHASH STAMP
nohup bash -c "
cd \"$PWD\" || exit 1
command -v ni >/dev/null 2>&1 || npm i -g @antfu/ni
ni --frozen
rc=\$?
[ \$rc -eq 0 ] && [ -n \"\$LOCKHASH\" ] && printf '%s' \"\$LOCKHASH\" > \"\$STAMP\"
printf '\n__DEPS_DONE__ exit=%s\n' \"\$rc\"
" >>"$LOG" 2>&1 </dev/null &
if [ -d node_modules ]; then
echo "📦 node_modules is out of sync with the lockfile — reinstalling in the background via 'ni --frozen'."
else
echo "📦 node_modules missing — installing deps in the background via 'ni --frozen' (ensuring ni via npm first if needed)."
fi
echo " Progress/log: $LOG"
echo " IMPORTANT: before running any code or tests, wait until that log ends with '__DEPS_DONE__ exit=0'."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment