diff --git a/CHANGELOG.md b/CHANGELOG.md index f88d7f53f..55fe8fccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,56 @@ # Changelog +## [1.68.1.0] - 2026-08-18 + +**Phantom hook errors are dead. Your settings.json now heals itself** +**on every setup, and no ephemeral path can ever be baked in again.** + +If you work in Conductor workspaces or git worktrees, you have probably seen it: `PostToolUse:AskUserQuestion hook error ... No such file or directory` spraying on every question, pointing at a workspace you deleted last week. The cause was a three-part failure. Setup baked the running tree's physical path into your global `~/.claude/settings.json`, the Conductor auto-opt-in overrode the exact flag `bin/dev-setup` passes to prevent that, and the dedupe tag gstack relied on gets stripped by Claude Code itself, so every new workspace appended a fresh dead entry instead of replacing the old one. + +All three are fixed at the root. Hook registration is now canonical-only: commands point at the stable `~/.claude/skills/gstack` install or are not registered at all. Ownership is decided by a fixed identity table in `bin/gstack-settings-hook`, per hook item, so it survives tag-stripping and can never claim a hook you wrote yourself. And every `./setup` run now heals first: `gstack-settings-hook prune-stale --repoint` removes dead gstack entries, re-points stale ones, restores stripped tags, and collapses duplicates, printing one line only when it changed something. + +### The numbers that matter + +Source: the 2026-08-17 incident on a real dev box, replayed byte-for-byte as the `incident facsimile` test in `test/gstack-settings-hook-schema-aware.test.ts`. + +| Metric | Before | After | Δ | +|--------|--------|-------|---| +| Hook entries in settings.json | 11 (6 dead) | 5, all canonical | −6 dead | +| Error lines per AskUserQuestion | 4 | 0 | −4 | +| Hook processes spawned per question that do nothing | 4 | 0 | −4 | +| Traced code paths under test | — | 53 of 61 (87%) | new | + +The healer also fixes damage you could not see: a corrupt settings.json is never overwritten (every mutator now fails closed instead of clobbering it with `{}`), a user-tightened 0600 file keeps its mode across rewrites (settings.json can carry API keys), concurrent setups can no longer rename a half-written temp file into place, and uninstall now cleans hooks BEFORE deleting the install root, which previously made cleanup silently no-op in exactly the case it existed for. + +### What this means for you + +Run `./setup` (or `/gstack-upgrade`) once and the errors stop, on every machine, with a printed receipt of what was healed and a backup beside the file. New workspaces can never reintroduce them. If you ever want everything gone, `gstack-uninstall` now actually removes every gstack hook, including the ones an older version orphaned. + +### Itemized changes + +### Added +- `gstack-settings-hook prune-stale [--repoint ] [--all]`: self-healing for hook registrations. Dead gstack entries pruned, stale paths re-pointed at the stable install, stripped `_gstack_source` tags restored from the identity table, exact duplicates and within-entry twins collapsed. Runs automatically at the start of every `./setup`; `--all` is the complete teardown sweep used by uninstall and `--no-team`. +- `gstack-config has `: key-presence check through the same state-dir resolution as `get` (which returns defaults for absent keys), so consent logic can tell a recorded decision from a default. +- KNOWN_HOOKS identity table covering all six gstack hooks (plan-tune trio, timeline Stop, session update, verify-gate), shared by registration dedupe and the healer so the two can never drift. +- A mutation lock around every settings.json write: mkdir-based with an owner token, ownership-checked release, and atomic stale-lock takeover. Backups get unique names and rotate (10 kept); `rollback` validates its pointer and restores atomically. + +### Changed +- Hook registration is canonical-only. Setup never writes a running-tree path into global settings; if the stable install is missing a hook, it skips with a visible log line instead. The Conductor auto-opt-in for AskUserQuestion reliability hooks now respects explicit decisions (flag, env, or a recorded config key) and fires only on the true silent fall-through. +- `add-event` is the single quoting authority: registered commands are normalized once (whitespace and shell metacharacters escaped), so a spaced or `$`-bearing install path produces a working hook from the first registration. Windows gets the required `bash ` prefix on all hooks, not just SessionStart, and MSYS-form paths no longer read as dead to the healer. +- All settings.json mutators are per-item: a hook you co-located in the same entry as a gstack hook survives every gstack operation, including uninstall, and gstack never tags an entry that contains your items. +- Teardown paths (`gstack-uninstall`, `./setup --no-team`) run hook cleanup before any deletion, sweep untagged strays by identity, and keep stderr attached so a skipped cleanup is loud, never silent. + +### Fixed +- Deleted Conductor workspaces and worktrees no longer leave dead hooks erroring on every AskUserQuestion, session start, and stop event. +- A corrupt settings.json is preserved and reported (exit 3) instead of being replaced with an empty object by the next hook operation. +- settings.json file mode is preserved across rewrites; fresh files are created 0600. +- Liveness checks treat only provable absence as dead, so an unmounted volume or permission blip cannot prune a working hook. +- A vacuous test in the banner-tripwire check executed its script through JSON-as-shell-quoting, silently littering a `2nelsen` artifact in the repo root on every suite run while asserting nothing; it now passes the script as argv and asserts both branches. + +### For contributors +- 60+ new or updated test cases across 8 files, including the incident facsimile, a two-writer concurrency smoke, an uninstall test that runs the installed copy from inside the root it deletes, held-lock teardown visibility, quoting round-trips, and static tripwires pinning canonical-only registration, heal-first ordering, matcher-literal parity, and the shared-prelude call sites. +- The review pipeline for this release (five specialists plus red team plus two Codex passes) contributed 14 verified hardening fixes; rejected findings are documented in the PR. + ## [1.67.1.0] - 2026-08-16 **We read every line of external-contributor code from the last two months.** diff --git a/TODOS.md b/TODOS.md index 38f69c9de..54a73c879 100644 --- a/TODOS.md +++ b/TODOS.md @@ -74,10 +74,19 @@ verified real but needs design input or device access the wave lacked: remote-tracking ref on every remote (stale experiment branches inflate the allocation) and a failed 3-digit base read flips width to 4. Warned today; tighten to origin + width-pin. Effort S. -- **Stop-hook registration pins the setup-time absolute path** — registering - from a dev worktree bakes that path into settings.json; deleting the - worktree leaves a dead hook erroring on every session stop until removed. - Register the global-install path or re-point on upgrade. Effort S. +- **setup:1601 CLAUDE_CONFIG_DIR alignment** — the skills installer hardcodes + `$HOME/.claude/skills` while settings.json and hook registration honor + `CLAUDE_CONFIG_DIR`; users with the override get a split-brain install. + Mitigated in v1.68.1 (canonical-root fallback to the home path so hooks + still register), but the installer itself should honor the override. + **Priority:** P3. Effort S. +- **Centralize plan_tune_hooks bool parsing + gstack-config key validation** — + the `n|no|false|skip|off|0` negative-value set is triplicated + (gstack-settings-hook prune-stale, setup heal note, setup PT_DECISION) and + gstack-config carries three verbatim copies of the key-validation block + (get/has/set). Extract a `gstack-config` bool helper + `validate_key()`; + update the locale pin test. Filed via /ship review army (maintainability). + **Priority:** P3. Effort S. - **Accepted threat-model notes (documented, no action planned):** redact-prepush treats content pushed to ANY private remote as already-left (accident-only threat model); a parcel-shaped twin within 400 chars can @@ -2698,6 +2707,21 @@ needs one paid run to validate, so it didn't ride the ship. ## Completed +### ✅ DONE (v1.68.1.0): Stop-hook registration pins the setup-time absolute path + +**Priority:** P1 (was filed Effort S, scoped to the Stop hook — shipped as the full defect class) + +**What:** Registering hooks from a dev worktree baked that worktree's physical +path into global settings.json; deleting the worktree left dead hooks erroring +on every AskUserQuestion/session stop. Fixed for ALL gstack hooks, not just +Stop: canonical-only registration via `_hook_command_path`, a KNOWN_HOOKS +identity table in `gstack-settings-hook` (survives Claude Code stripping +`_gstack_source` tags), a `prune-stale [--repoint|--all]` self-healer that +runs heal-first on every `./setup`, per-item mutation safety, a mutation lock, +fail-closed parse, and complete uninstall/no-team teardown. + +**Completed:** v1.68.1.0 (2026-08-18) + ### ✅ DONE (v1.66.0.0): Free suite exit code is untrustworthy — in-process force-exits mask failures **Priority:** P1 diff --git a/VERSION b/VERSION index c05817ad8..0569dac5b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.67.1.0 +1.68.1.0 diff --git a/package.json b/package.json index b0dfa29e5..b200543cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.67.1.0", + "version": "1.68.1", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module",