#!/usr/bin/env bash # SessionStart hook: refuse to let a severed dotfiles link go unnoticed. # # Claude Code writes settings with an atomic temp+rename. Through the stow layer that can # replace ~/.config/claude/ (a symlink into the repo) with a plain FILE holding whatever # snapshot the writing process had in memory -- silently freezing every later repo edit out # of the live config. It happened on 2026-09-15 23:31: a stale copy dropped four hook events # (git-guard among them) and nobody noticed for an hour. link.sh heals the identical case but # only when run; this makes every session start say so instead. # # Read-only: it never relinks. Exit 2 on SessionStart shows stderr to the user and the session # continues. Exit 0 when everything resolves into the repo. set -u src="$HOME/.config/claude" bad="" for f in settings.json statusline.py keybindings.json CLAUDE.md link.sh; do p="$src/$f" [ -e "$p" ] || continue if [ ! -L "$p" ]; then bad="$bad $p is a plain file (should be a symlink into the dots repo)"$'\n'; fi done # the ~/.claude side too: link.sh's own guard covers it, but only when link.sh runs for f in settings.json keybindings.json CLAUDE.md statusline.py; do p="$HOME/.claude/$f" [ -e "$p" ] || continue case "$(readlink -f "$p")" in *"/.dots/"*) ;; *) bad="$bad $p resolves to $(readlink -f "$p"), not into the dots repo"$'\n' ;; esac done [ -z "$bad" ] && exit 0 printf 'DRIFT: Claude config has broken out of the dots repo; the live config is a stale copy.\n%s fix: review with `diff`, then `bash ~/.config/claude/link.sh` (heals identical copies, warns otherwise)\n' "$bad" >&2 exit 2