From ca65bbabd653ed97bf03c6e57d54d27dc3f9ebc9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 13:13:55 -0800 Subject: [PATCH 1/5] quest: plan dev shells from user settings instead of repo hooks The maintainer won't install quest on Claude's PATH and doesn't want repository config to force direnv, Nix or quest on contributors. Plan the replacement: user-level dev-shell hooks, repositories dropping their dev-shell and worktree hooks and nix instructions, and quest init writing no hook entries. Drops worktree-hooks and wrapped-hooks, and moves session-checkouts onto the opt-in user-level setup. Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 3 +- quest/m0/session-checkouts.md | 7 ++- quest/m0/user-dev-shell/README.md | 59 ++++++++++++++++++++++++++ quest/m0/user-dev-shell/init-hooks.md | 29 +++++++++++++ quest/m0/user-dev-shell/repo-config.md | 31 ++++++++++++++ quest/m0/worktree-hooks.md | 29 ------------- quest/m0/worktree-routes.md | 4 +- quest/m0/wrapped-hooks.md | 40 ----------------- 8 files changed, 128 insertions(+), 74 deletions(-) create mode 100644 quest/m0/user-dev-shell/README.md create mode 100644 quest/m0/user-dev-shell/init-hooks.md create mode 100644 quest/m0/user-dev-shell/repo-config.md delete mode 100644 quest/m0/worktree-hooks.md delete mode 100644 quest/m0/wrapped-hooks.md diff --git a/quest/m0/README.md b/quest/m0/README.md index 84053cb0..aeee766b 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -104,8 +104,7 @@ ownership-manifest design: - [Gate bench](/quest/m0/gate-bench.md) - measure headroom admission on an idle host - [Gate load wording](/quest/m0/gate-load-wording.md) - stop naming Spotlight as the cause of a fresh macOS runner's load - [Agent workflow helpers](/quest/m0/helpers/README.md) - CLI commands for CI waits, PR status, claims, quest moves, and serialized merges -- [Worktree hooks here](/quest/m0/worktree-hooks.md) - `quest` reaches Claude Code's hooks and the merge driver in this repository, then `quest init` installs the hooks -- [Wrapped worktree hooks](/quest/m0/wrapped-hooks.md) - `quest init` accepts a hook that runs `… worktree hook` through a wrapper such as `nix run` +- [Agents get the dev shell from user settings](/quest/m0/user-dev-shell/README.md) - maintainers' own Claude and Codex settings load the dev shell; repositories drop dev-shell hooks, Quest worktree hooks, and `nix develop` instructions, and `quest init` writes no hooks - [Remote control worktrees](/quest/m0/worktree-routes.md) - live-check `claude remote-control --spawn worktree` against the hooks, from a session spawned on claude.ai - [Session checkouts released](/quest/m0/session-checkouts.md) - a Claude session's own checkout is released when the session ends and reclaimed on resume - [claude rm upstream report](/quest/m0/claude-rm-upstream.md) - waiting on approval: ask Claude Code to run `WorktreeRemove` instead of advising an unlock diff --git a/quest/m0/session-checkouts.md b/quest/m0/session-checkouts.md index 01d06a83..94ccac56 100644 --- a/quest/m0/session-checkouts.md +++ b/quest/m0/session-checkouts.md @@ -23,8 +23,11 @@ Evidence, gathered 2026-11-02: Work: -- Install `SessionEnd` and `SessionStart` hook entries through - `quest init`, recognised by `init` and `uninstall` like the others. +- Add `SessionEnd` and `SessionStart` entries to the opt-in user-level hook + setup the docs describe. Amended 2026-11-03 (maintainer, /quest-unblock): + `quest init` writes no hook entries any more + ([quest init writes no hooks](/quest/m0/user-dev-shell/init-hooks.md)), + so this quest no longer installs them through `init`. - `SessionEnd` releases (never removes) the session's checkout; `SessionStart` claims it again on resume. `/clear` also fires `SessionEnd`, so release is idempotent and the next `SessionStart` diff --git a/quest/m0/user-dev-shell/README.md b/quest/m0/user-dev-shell/README.md new file mode 100644 index 00000000..bde925bb --- /dev/null +++ b/quest/m0/user-dev-shell/README.md @@ -0,0 +1,59 @@ +# Agents get the dev shell from user settings, not repository config + +## Goal + +Agents in moq-dev's repositories run tools without caring about Nix: each +maintainer's own Claude Code and Codex settings load the repository's dev +shell into every session's commands, so no repository forces direnv, Nix, or +`quest` on its contributors through checked-in hooks. No repository carries a +dev-shell `SessionStart` hook, a Quest worktree hook, or AGENTS.md +instructions to wrap commands in `nix develop`, and `quest init` writes no +hook entries. + +## Plan + +Decided 2026-11-03 (maintainer, /quest-unblock of +[moq-dev/quest#423](https://github.com/moq-dev/quest/pull/423)'s wait to +install `quest` on Claude's `PATH`): + +- Install `quest` on the `PATH` Claude Code starts with? ✅ No (rejected: + `nix profile add github:moq-dev/quest` on every host). +- How should sessions reach a pinned `quest` and the rest of the dev shell? + ✅ A dev-shell hook in the maintainer's own Claude settings (rejected: hook + entries that `nix run` the repository's pinned input, as moq-dev/moq does; + dropping the hooks with no replacement). Reason: "We don't want to force + direnv+nix for other users"; repository config applies to every + contributor. +- Repository-level direnv `SessionStart` hooks in quest, moq, moq.pro, rind, + and rust2? ✅ Remove them from all five once the user-level hook loads the + dev shell (rejected: keep them). +- Quest's worktree hooks? ✅ Drop them from repositories, and `quest init` + stops writing hook entries; the hooks become an opt-in user-level setup + (rejected: this repository and moq only; this repository only; moving them + into the maintainer's settings). + [moq-dev/quest#423](https://github.com/moq-dev/quest/pull/423) is closed. +- Codex? ✅ A user-level Codex `SessionStart` hook does the same capture, and + the repositories' `.codex/` dev-shell hooks go (rejected: Claude only). +- A fresh worktree needs `direnv allow`: direnv keys each approval by the + SHA-256 of the `.envrc`'s absolute path and contents + (`~/.local/share/direnv/allow/`, 1,706 entries on the maintainer's host on + 2026-11-03), so nothing carries over from the main checkout. ✅ The + user-level hook allows a checkout itself when its `.envrc`, `flake.nix`, + and `flake.lock` match the remote's default branch (rejected: whitelisting + `~/work` and `~/worktrees` in `direnv.toml`, which would also evaluate a + fork PR's dev shell; keeping manual allows). A branch that changes them, + or a fork PR head, still needs a manual allow. +- AGENTS.md instructions to run tools through `nix develop`? ✅ Remove them in + all five repositories once the user-level hooks are on every host the + maintainer runs agents on (rejected: this repository only; keeping them as + a fallback). + +The user-level hooks live in [kixelated/dotfiles](https://github.com/kixelated/dotfiles), +installed into each Claude profile's `settings.json` and Codex's user +config, and generalize this repository's `.claude/hooks/direnv.sh` and +moq-dev/moq's `.codex/hooks/direnv.sh`. + +## Required + +- [quest init writes no hooks](/quest/m0/user-dev-shell/init-hooks.md) - `quest init` stops writing hook entries and removes the ones it or a wrapper wrote; the docs describe the hooks as user-level opt-in +- [Repositories drop dev-shell config](/quest/m0/user-dev-shell/repo-config.md) - quest, moq, moq.pro, rind, and rust2 drop their dev-shell hooks, Quest worktree hooks, and `nix develop` instructions diff --git a/quest/m0/user-dev-shell/init-hooks.md b/quest/m0/user-dev-shell/init-hooks.md new file mode 100644 index 00000000..5d953913 --- /dev/null +++ b/quest/m0/user-dev-shell/init-hooks.md @@ -0,0 +1,29 @@ +# [S] quest init writes no hook entries + +## Goal + +`quest init` no longer writes Claude Code hook entries into a repository's +`.claude/settings.json`, and removes the `quest worktree hook` entries an +earlier version wrote, including ones a repository wrapped, such as +moq-dev/moq's `nix run "${CLAUDE_PROJECT_DIR}#quest" -- worktree hook`. +`quest uninstall` removes both forms too. The README and SETUP.md describe the +worktree hooks as an opt-in a user adds to their own Claude settings, with +the entries to add. + +## Plan + +Decided 2026-11-03 (maintainer, /quest-unblock): repository config applies +to every contributor, so Quest's hooks become user-level opt-in; see the +[epic](/quest/m0/user-dev-shell/README.md). + +- Replaces the wrapped-hooks quest (moq-dev/moq, 2026-11-03), which widened + `isQuestHook` (`src/core/init.ts`) so `init` would accept wrapped hooks. + Keep its matching rule for removal: a command hook whose last two words + are `worktree hook` is Quest's. +- `HOOK_EVENTS` and the hook plan in `src/core/init.ts` stop adding + entries; `init` reports each entry it removes. The enrollment setup PR + ([moq-dev/quest#431](https://github.com/moq-dev/quest/pull/431)) uses the + same plan, so it stops proposing hooks too. +- The `quest worktree hook` command stays, for users who opt in. +- Test `init` and `uninstall` on a repository with plain and wrapped + entries, and with other hooks beside them, which stay. diff --git a/quest/m0/user-dev-shell/repo-config.md b/quest/m0/user-dev-shell/repo-config.md new file mode 100644 index 00000000..2d5525de --- /dev/null +++ b/quest/m0/user-dev-shell/repo-config.md @@ -0,0 +1,31 @@ +# [S] Repositories drop their dev-shell config + +## Goal + +moq-dev/quest, moq, moq.pro, rind, and rust2 carry no dev-shell hooks and no +instructions to wrap commands in Nix: each drops its direnv `SessionStart` +hook from `.claude/settings.json` and `.codex/hooks.json` with the scripts +and tests behind them, moq drops its four Quest worktree hook entries, and +each AGENTS.md drops its `nix develop` instructions. Agents there still run +the pinned tools, through the maintainer's user-level hooks. One PR per +repository. + +## Plan + +Decided 2026-11-03 (maintainer, /quest-unblock); see the +[epic](/quest/m0/user-dev-shell/README.md). + +- Keep every other hook: moq.pro's `submodule.sh`, `worktree.sh` (its own + submodule-aware worktrees, not Quest's), and `tofu-secrets.py`. +- CONTRIBUTING.md's human setup (`nix develop` to match CI) stays; only the + agent-facing instructions go. In this repository, AGENTS.md's "`quest` + means `bun run --silent quest`" note stays, since it names which `quest` + to run, not how to load it. +- Check each repository from a fresh worktree on a host with the user-level + hooks: a new Claude Code session and a new Codex session both resolve the + dev shell's tools (`which just`, `quest --version` where pinned) with no + `nix develop` and no manual `direnv allow`. + +## Waiting + +- Maintainer: pull kixelated/dotfiles and run its `install` on every host that runs agents (`nixos`, `nixos2`, `desktop`, `kixtop`), since the repositories' hooks must not go before the user-level ones arrive; check on each host that `~/.claude-personal/settings.json` has the dev-shell `SessionStart` hook and that a fresh worktree's session resolves `just` diff --git a/quest/m0/worktree-hooks.md b/quest/m0/worktree-hooks.md deleted file mode 100644 index 0dd9d2eb..00000000 --- a/quest/m0/worktree-hooks.md +++ /dev/null @@ -1,29 +0,0 @@ -# [S] This repository installs its own worktree hooks - -## Goal - -Claude Code sessions in moq-dev/quest reclaim their subagents' checkouts -through `quest worktree hook`, as `quest init` sets up in other -repositories. `quest` is reliably available to Claude Code's hook -environment here, and on `PATH` for the merge driver, without running a PR -head's code. `quest init` then installs the hooks in this repository's -`.claude/settings.json`. - -## Plan - -Decided 2026-11-01 (user, /quest-plan of the follow-ups from the -2026-11-01 `/quest-complete` sessions): ✅ plan it, [S], m0. Sources: -[moq-dev/quest#350](https://github.com/moq-dev/quest/pull/350)'s decision 1a -and [moq-dev/quest#359](https://github.com/moq-dev/quest/pull/359). - -- [moq-dev/quest#350](https://github.com/moq-dev/quest/pull/350) shipped the - hooks but, by its decision 1a, didn't install them here: Claude Code runs - hooks in its own environment, not the direnv dev shell, and the merge - driver also needs `quest` on `PATH`. This repository's 65 leaked - worktrees (8.2 GB, 2026-10-23) were the measurement behind the hooks. -- The dev shell's `quest` is the reviewed `quest-pinned` build - ([moq-dev/quest#344](https://github.com/moq-dev/quest/pull/344)), while - `bun run --silent quest` runs the checkout. Choose how hooks reach a - pinned `quest` outside the dev shell, for example an installed release - that `SETUP.md` asks contributors for, and say what happens for a - contributor without one. diff --git a/quest/m0/worktree-routes.md b/quest/m0/worktree-routes.md index e0cbefbf..44ef6442 100644 --- a/quest/m0/worktree-routes.md +++ b/quest/m0/worktree-routes.md @@ -23,7 +23,9 @@ agent can't do, so its README description comes from Claude Code's own code: - The bridge never runs `git worktree unlock` on a hook-created checkout, since it records no Git root for one, so quest's lock stays in place. -Check it from a scratch repository set up with `quest init`, with +Check it from a scratch repository with the worktree hooks configured (the +opt-in user-level setup once +[quest init writes no hooks](/quest/m0/user-dev-shell/init-hooks.md) lands), with `QUEST_WORKTREE_HOME` isolated: start `claude remote-control --spawn worktree`, spawn a session from claude.ai, end it once without changes and once with an untracked file, and watch `quest worktree status`. Fix the README if it diff --git a/quest/m0/wrapped-hooks.md b/quest/m0/wrapped-hooks.md deleted file mode 100644 index 57bd248d..00000000 --- a/quest/m0/wrapped-hooks.md +++ /dev/null @@ -1,40 +0,0 @@ -# [XS] quest init recognizes wrapped worktree hooks - -## Goal - -`quest init` treats any command hook whose arguments end in `worktree hook` -as its own, such as moq-dev/moq's -`nix run "${CLAUDE_PROJECT_DIR}#quest" -- worktree hook`, which runs the -`quest` the repository pins. It needs no configuration. In a repository with -wrapped hooks, `quest init` then succeeds without changing them, and -`quest uninstall` removes them like the plain ones. - -## Plan - -Decided 2026-11-03 (maintainer, moq-dev/moq, planned through moq-dev/moq's -coordinator): - -- How should `quest init` recognize its hook when a repository wraps it? - - ✅ Match any command hook whose arguments end in `worktree hook` - - A configured hook command (rejected) - - No change (rejected) - -Milestone: m0, ranked after [Worktree hooks here](/quest/m0/worktree-hooks.md). -m0 is adoption on moq-dev's own repositories, and moq-dev/moq hits this -today. - -What happens: -[moq-dev/moq#7674](https://github.com/moq-dev/moq/pull/7674) runs moq's hooks -through its flake's pinned `quest`, so agents use the version moq pins. -`isQuestHook` (`src/core/init.ts`) matches only the exact `HOOK_COMMAND`, -`quest worktree hook`. So `quest init` counts the wrapped `WorktreeCreate` -and `WorktreeRemove` hooks as someone else's and refuses to run. The -workaround is to drop the hooks, run init, and restore them. - -Implementation guidance: - -- Widen `isQuestHook` to a command whose last two words are `worktree hook`. - A fresh install still writes the plain `HOOK_COMMAND`. -- `quest uninstall` (`src/cli/setup.ts`) uses the same check, so it removes - wrapped hooks too. Say so in its docs. -- Test a wrapped hook in init, re-init, and uninstall. From 763704a8a3f6a2c11bb60d695347711fa3ff460c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 13:29:10 -0800 Subject: [PATCH 2/5] quest: name the dotfiles PR the repo-config wait depends on Co-Authored-By: Claude Opus 5.5 --- quest/m0/user-dev-shell/repo-config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quest/m0/user-dev-shell/repo-config.md b/quest/m0/user-dev-shell/repo-config.md index 2d5525de..0d62e277 100644 --- a/quest/m0/user-dev-shell/repo-config.md +++ b/quest/m0/user-dev-shell/repo-config.md @@ -28,4 +28,4 @@ Decided 2026-11-03 (maintainer, /quest-unblock); see the ## Waiting -- Maintainer: pull kixelated/dotfiles and run its `install` on every host that runs agents (`nixos`, `nixos2`, `desktop`, `kixtop`), since the repositories' hooks must not go before the user-level ones arrive; check on each host that `~/.claude-personal/settings.json` has the dev-shell `SessionStart` hook and that a fresh worktree's session resolves `just` +- Maintainer: merge [kixelated/dotfiles#33](https://github.com/kixelated/dotfiles/pull/33) (the user-level hooks; live on the `desktop` host since 2026-11-03), then pull kixelated/dotfiles and run `bin/install-dev-shell.py` (or the full `install`, with `codex` on `PATH`) on every host that runs agents (`nixos`, `nixos2`, `desktop`, `kixtop`), since the repositories' hooks must not go before the user-level ones arrive; check on each host that `~/.claude-personal/settings.json` has the dev-shell `SessionStart` hook and that a fresh worktree's session resolves `just` From 1635b55bfae112c6b017c06206f4c958224d0fbf Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 13:59:22 -0800 Subject: [PATCH 3/5] quest: narrow the dev-shell hook's auto-allow and settle review findings The hook's three-file match auto-allowed any repository's fresh clone and a fork PR head that changes another file the dev shell runs; a new child quest records the reproduction and the maintainer's choice of a safe rule. Also clarify that session-end release keeps remote-control checkouts' files and lock, record that removing moq's wrapped hooks has no user-level replacement, and refresh repo-config's wait now that kixelated/dotfiles#33 is merged. Co-Authored-By: Claude Opus 5.5 --- quest/m0/session-checkouts.md | 11 ++++++- quest/m0/user-dev-shell/README.md | 12 ++++++-- quest/m0/user-dev-shell/allow-rule.md | 42 ++++++++++++++++++++++++++ quest/m0/user-dev-shell/init-hooks.md | 9 ++++++ quest/m0/user-dev-shell/repo-config.md | 6 +++- 5 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 quest/m0/user-dev-shell/allow-rule.md diff --git a/quest/m0/session-checkouts.md b/quest/m0/session-checkouts.md index 94ccac56..3c9cbce8 100644 --- a/quest/m0/session-checkouts.md +++ b/quest/m0/session-checkouts.md @@ -25,13 +25,22 @@ Work: - Add `SessionEnd` and `SessionStart` entries to the opt-in user-level hook setup the docs describe. Amended 2026-11-03 (maintainer, /quest-unblock): - `quest init` writes no hook entries any more + `quest init` writes no hook entries anymore ([quest init writes no hooks](/quest/m0/user-dev-shell/init-hooks.md)), so this quest no longer installs them through `init`. - `SessionEnd` releases (never removes) the session's checkout; `SessionStart` claims it again on resume. `/clear` also fires `SessionEnd`, so release is idempotent and the next `SessionStart` reclaims it. An idle background session never ends; leave it held. +- Remote control's `bridge-*` checkouts are session checkouts too, and the + release covers them if `SessionEnd` fires in a bridge-spawned session. + [Remote control's live check](/quest/m0/worktree-routes.md) confirms + whether it does. Release only prunes the declared scratch: it keeps every + other file and Git's worktree lock (`release` and `prune` in + `src/cli/worktree.ts`; only `remove` unlocks). The bridge still decides at + session end, as that quest describes: it removes a clean checkout through + `WorktreeRemove` and keeps one with changes. Verify that order in the same + live check. - Claude Code bounds `SessionEnd` hooks by a short budget (`CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS`) and does not wait past it. Measure a release with scratch to delete, and set the entry's timeout to diff --git a/quest/m0/user-dev-shell/README.md b/quest/m0/user-dev-shell/README.md index bde925bb..33aaa795 100644 --- a/quest/m0/user-dev-shell/README.md +++ b/quest/m0/user-dev-shell/README.md @@ -41,8 +41,15 @@ install `quest` on Claude's `PATH`): user-level hook allows a checkout itself when its `.envrc`, `flake.nix`, and `flake.lock` match the remote's default branch (rejected: whitelisting `~/work` and `~/worktrees` in `direnv.toml`, which would also evaluate a - fork PR's dev shell; keeping manual allows). A branch that changes them, - or a fork PR head, still needs a manual allow. + fork PR's dev shell; keeping manual allows). A branch that changes them + still needs a manual allow. Amended 2026-11-03 (review of + [moq-dev/quest#458](https://github.com/moq-dev/quest/pull/458)): those + three files alone don't make a checkout reviewed. Any repository's fresh + clone matches its own default branch. A fork PR head can change another + file the dev shell runs, such as moq.pro's `dev/mbx-shim.sh`. Both were + auto-allowed in a reproduction against the merged hook. + [The allow rule](/quest/m0/user-dev-shell/allow-rule.md) narrows it to + trusted repositories and reviewed code. - AGENTS.md instructions to run tools through `nix develop`? ✅ Remove them in all five repositories once the user-level hooks are on every host the maintainer runs agents on (rejected: this repository only; keeping them as @@ -55,5 +62,6 @@ moq-dev/moq's `.codex/hooks/direnv.sh`. ## Required +- [The dev-shell hook allows only trusted, reviewed checkouts](/quest/m0/user-dev-shell/allow-rule.md) - the user-level hook's automatic `direnv allow` skips other repositories' clones and fork PR heads - [quest init writes no hooks](/quest/m0/user-dev-shell/init-hooks.md) - `quest init` stops writing hook entries and removes the ones it or a wrapper wrote; the docs describe the hooks as user-level opt-in - [Repositories drop dev-shell config](/quest/m0/user-dev-shell/repo-config.md) - quest, moq, moq.pro, rind, and rust2 drop their dev-shell hooks, Quest worktree hooks, and `nix develop` instructions diff --git a/quest/m0/user-dev-shell/allow-rule.md b/quest/m0/user-dev-shell/allow-rule.md new file mode 100644 index 00000000..e6cc5f7a --- /dev/null +++ b/quest/m0/user-dev-shell/allow-rule.md @@ -0,0 +1,42 @@ +# [S] The dev-shell hook allows only trusted, reviewed checkouts + +## Goal + +The maintainer's user-level dev-shell hook (`bin/dev-shell-hook` in +[kixelated/dotfiles](https://github.com/kixelated/dotfiles)) runs +`direnv allow` on its own only for a checkout of a repository the maintainer +trusts, at code that someone trusted reviewed or pushed. A fresh clone of any +other repository, a fork PR head, and a checkout whose dev-shell files differ +from the default branch still wait for a manual `direnv allow`, and the hook +says why on stderr. + +## Plan + +Found 2026-11-03 by CodeRabbit's review of +[moq-dev/quest#458](https://github.com/moq-dev/quest/pull/458#discussion_r4460595569) +and reproduced against `bin/dev-shell-hook` as merged in +[kixelated/dotfiles#33](https://github.com/kixelated/dotfiles/pull/33) +(blob `679997db`). The hook approves an unapproved `.envrc` when `.envrc`, +`flake.nix`, and `flake.lock` match `refs/remotes/origin/HEAD`, which +it never fetches. Each case below ran from an isolated `XDG_DATA_HOME`: + +- **Any repository's fresh clone:** `origin` is whatever was cloned, so its + files match its own default branch by construction. A clone whose `.envrc` + writes a file was allowed and ran. The hook runs in every Claude Code and + Codex session, not just moq-dev's. A user-level hook also runs whether or + not the project's own hooks are trusted. +- **A branch that changes another file the dev shell runs:** an `.envrc` that + sources `dev/hook.sh`, on a branch that changes only `dev/hook.sh`, was + allowed and ran the branch's script. moq-dev/moq.pro's dev shell has this + shape: its `shellHook` inlines `builtins.readFile ./dev/mbx-shim.sh`. A fork + PR head in a trusted clone is such a branch. The maintainer's moq clone + keeps four contributor remotes. +- A changed `.envrc` was refused, as intended. + +So the epic's claim that a fork PR head needs a manual allow does not hold. +The work is in kixelated/dotfiles: `bin/dev-shell-hook`, its docstring, and +the README that describes it. + +## Waiting + +- Maintainer: pick the hook's allow rule. An agent here can't edit the maintainer's live dotfiles without this choice. A: trust a repository by its identity and by who pushed the code. Auto-allow only when `git remote get-url origin` names a GitHub repository under an allowlisted owner (`moq-dev`, `kixelated`); HEAD is reachable from a branch on `origin` (`git for-each-ref --contains HEAD refs/remotes/origin/`), so someone with write access pushed it; tracked files have no uncommitted changes, submodules included; `.envrc` is tracked; and the three files still match `origin/HEAD`. This keeps fresh worktrees and `/quest-iterate` on write-access PR branches automatic, matching the guide's rule for running a PR's code (recommended). B: same identity check, but only when the tracked tree equals the merge base of HEAD and `origin/HEAD`, so only reviewed, merged code loads. Every session on a branch with changes then needs a manual allow, and once [repo-config](/quest/m0/user-dev-shell/repo-config.md) drops the `nix develop` instructions those sessions lose their documented fallback. C: identity check only, keeping today's three-file match. This closes third-party clones but still auto-allows a fork PR head whose diff touches only a file like moq.pro's `dev/mbx-shim.sh`. Done once the change is merged in kixelated/dotfiles and `bin/install-dev-shell.py` has run on every host that has the hook diff --git a/quest/m0/user-dev-shell/init-hooks.md b/quest/m0/user-dev-shell/init-hooks.md index 5d953913..d2b78401 100644 --- a/quest/m0/user-dev-shell/init-hooks.md +++ b/quest/m0/user-dev-shell/init-hooks.md @@ -25,5 +25,14 @@ to every contributor, so Quest's hooks become user-level opt-in; see the ([moq-dev/quest#431](https://github.com/moq-dev/quest/pull/431)) uses the same plan, so it stops proposing hooks too. - The `quest worktree hook` command stays, for users who opt in. +- Removing moq's four wrapped entries, whether `init` here or + [repo-config](/quest/m0/user-dev-shell/repo-config.md) does it first, ends + Quest's reclaim of Claude Code checkouts in moq. Nothing replaces it: the + maintainer's user-level setup (kixelated/dotfiles) loads only the dev + shell, and the epic's decision rejected moving Quest's hooks into the + maintainer's settings. So this quest doesn't wait on that setup, and the + same end state results in either order. A user who wants reclaim adds the + opt-in entries the docs describe. Any removal through the enrollment setup + PR shows in that PR's diff. - Test `init` and `uninstall` on a repository with plain and wrapped entries, and with other hooks beside them, which stay. diff --git a/quest/m0/user-dev-shell/repo-config.md b/quest/m0/user-dev-shell/repo-config.md index 0d62e277..4bd11ac0 100644 --- a/quest/m0/user-dev-shell/repo-config.md +++ b/quest/m0/user-dev-shell/repo-config.md @@ -28,4 +28,8 @@ Decided 2026-11-03 (maintainer, /quest-unblock); see the ## Waiting -- Maintainer: merge [kixelated/dotfiles#33](https://github.com/kixelated/dotfiles/pull/33) (the user-level hooks; live on the `desktop` host since 2026-11-03), then pull kixelated/dotfiles and run `bin/install-dev-shell.py` (or the full `install`, with `codex` on `PATH`) on every host that runs agents (`nixos`, `nixos2`, `desktop`, `kixtop`), since the repositories' hooks must not go before the user-level ones arrive; check on each host that `~/.claude-personal/settings.json` has the dev-shell `SessionStart` hook and that a fresh worktree's session resolves `just` +- Maintainer: pull kixelated/dotfiles and run `bin/install-dev-shell.py` (or the full `install`, with `codex` on `PATH`) on `nixos` and `kixtop`; it changes the live agent config on each host, which an agent here doesn't touch unasked. The repositories' hooks must not go before the user-level ones arrive. [kixelated/dotfiles#33](https://github.com/kixelated/dotfiles/pull/33) is merged. On 2026-11-03, `desktop` and `nixos2` had the hook in `~/.claude-personal/settings.json`, but `nixos` and `kixtop` didn't. Check on each host that `~/.claude-personal/settings.json` has the dev-shell `SessionStart` hook and that a fresh worktree's session resolves `just` + +## Required + +- [The dev-shell hook allows only trusted, reviewed checkouts](/quest/m0/user-dev-shell/allow-rule.md) - its allow rule decides which sessions load the dev shell without a manual `direnv allow`, so it settles before the repositories drop their fallbacks From 7525141f57bc262b2c472a947bb19218de96162b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 14:15:24 -0800 Subject: [PATCH 4/5] quest: settle the dev-shell hook's allow rule with one guard The maintainer judged the auto-allow risk narrow and picked one check: HEAD must be on an origin branch, shipped in kixelated/dotfiles#35. Record the decision in the epic, delete the allow-rule quest it settles, and drop kixtop from repo-config's wait since it has no direnv or Nix. Co-Authored-By: Claude Opus 5.5 --- quest/m0/user-dev-shell/README.md | 16 ++++++++-- quest/m0/user-dev-shell/allow-rule.md | 42 -------------------------- quest/m0/user-dev-shell/repo-config.md | 6 +--- 3 files changed, 14 insertions(+), 50 deletions(-) delete mode 100644 quest/m0/user-dev-shell/allow-rule.md diff --git a/quest/m0/user-dev-shell/README.md b/quest/m0/user-dev-shell/README.md index 33aaa795..441ea38c 100644 --- a/quest/m0/user-dev-shell/README.md +++ b/quest/m0/user-dev-shell/README.md @@ -48,8 +48,19 @@ install `quest` on Claude's `PATH`): clone matches its own default branch. A fork PR head can change another file the dev shell runs, such as moq.pro's `dev/mbx-shim.sh`. Both were auto-allowed in a reproduction against the merged hook. - [The allow rule](/quest/m0/user-dev-shell/allow-rule.md) narrows it to - trusted repositories and reviewed code. + Decided 2026-11-03 (maintainer, /quest-complete), after weighing the + risk as narrow: a session rooted at an outside contributor's fork head, + which the guide already says never to run, since agents check untrusted + heads out under `.tmp/` rather than as a session root, and an agent + session in any checkout runs its code anyway. ✅ One cheap guard: the + hook also requires `HEAD` to be on some `origin` branch + (`git for-each-ref --contains HEAD refs/remotes/origin/`), so a fork PR + head is never auto-allowed (rejected: an owner allowlist, a clean tree, + and a tracked `.envrc` on top; matching the whole tree to the merge base; + an owner allowlist alone; leaving it as is). Shipped in + [kixelated/dotfiles#35](https://github.com/kixelated/dotfiles/pull/35) + and live on `desktop` and `nixos2`, where a fresh worktree of moq-dev/moq + still loads and one on an unpushed commit is refused. - AGENTS.md instructions to run tools through `nix develop`? ✅ Remove them in all five repositories once the user-level hooks are on every host the maintainer runs agents on (rejected: this repository only; keeping them as @@ -62,6 +73,5 @@ moq-dev/moq's `.codex/hooks/direnv.sh`. ## Required -- [The dev-shell hook allows only trusted, reviewed checkouts](/quest/m0/user-dev-shell/allow-rule.md) - the user-level hook's automatic `direnv allow` skips other repositories' clones and fork PR heads - [quest init writes no hooks](/quest/m0/user-dev-shell/init-hooks.md) - `quest init` stops writing hook entries and removes the ones it or a wrapper wrote; the docs describe the hooks as user-level opt-in - [Repositories drop dev-shell config](/quest/m0/user-dev-shell/repo-config.md) - quest, moq, moq.pro, rind, and rust2 drop their dev-shell hooks, Quest worktree hooks, and `nix develop` instructions diff --git a/quest/m0/user-dev-shell/allow-rule.md b/quest/m0/user-dev-shell/allow-rule.md deleted file mode 100644 index e6cc5f7a..00000000 --- a/quest/m0/user-dev-shell/allow-rule.md +++ /dev/null @@ -1,42 +0,0 @@ -# [S] The dev-shell hook allows only trusted, reviewed checkouts - -## Goal - -The maintainer's user-level dev-shell hook (`bin/dev-shell-hook` in -[kixelated/dotfiles](https://github.com/kixelated/dotfiles)) runs -`direnv allow` on its own only for a checkout of a repository the maintainer -trusts, at code that someone trusted reviewed or pushed. A fresh clone of any -other repository, a fork PR head, and a checkout whose dev-shell files differ -from the default branch still wait for a manual `direnv allow`, and the hook -says why on stderr. - -## Plan - -Found 2026-11-03 by CodeRabbit's review of -[moq-dev/quest#458](https://github.com/moq-dev/quest/pull/458#discussion_r4460595569) -and reproduced against `bin/dev-shell-hook` as merged in -[kixelated/dotfiles#33](https://github.com/kixelated/dotfiles/pull/33) -(blob `679997db`). The hook approves an unapproved `.envrc` when `.envrc`, -`flake.nix`, and `flake.lock` match `refs/remotes/origin/HEAD`, which -it never fetches. Each case below ran from an isolated `XDG_DATA_HOME`: - -- **Any repository's fresh clone:** `origin` is whatever was cloned, so its - files match its own default branch by construction. A clone whose `.envrc` - writes a file was allowed and ran. The hook runs in every Claude Code and - Codex session, not just moq-dev's. A user-level hook also runs whether or - not the project's own hooks are trusted. -- **A branch that changes another file the dev shell runs:** an `.envrc` that - sources `dev/hook.sh`, on a branch that changes only `dev/hook.sh`, was - allowed and ran the branch's script. moq-dev/moq.pro's dev shell has this - shape: its `shellHook` inlines `builtins.readFile ./dev/mbx-shim.sh`. A fork - PR head in a trusted clone is such a branch. The maintainer's moq clone - keeps four contributor remotes. -- A changed `.envrc` was refused, as intended. - -So the epic's claim that a fork PR head needs a manual allow does not hold. -The work is in kixelated/dotfiles: `bin/dev-shell-hook`, its docstring, and -the README that describes it. - -## Waiting - -- Maintainer: pick the hook's allow rule. An agent here can't edit the maintainer's live dotfiles without this choice. A: trust a repository by its identity and by who pushed the code. Auto-allow only when `git remote get-url origin` names a GitHub repository under an allowlisted owner (`moq-dev`, `kixelated`); HEAD is reachable from a branch on `origin` (`git for-each-ref --contains HEAD refs/remotes/origin/`), so someone with write access pushed it; tracked files have no uncommitted changes, submodules included; `.envrc` is tracked; and the three files still match `origin/HEAD`. This keeps fresh worktrees and `/quest-iterate` on write-access PR branches automatic, matching the guide's rule for running a PR's code (recommended). B: same identity check, but only when the tracked tree equals the merge base of HEAD and `origin/HEAD`, so only reviewed, merged code loads. Every session on a branch with changes then needs a manual allow, and once [repo-config](/quest/m0/user-dev-shell/repo-config.md) drops the `nix develop` instructions those sessions lose their documented fallback. C: identity check only, keeping today's three-file match. This closes third-party clones but still auto-allows a fork PR head whose diff touches only a file like moq.pro's `dev/mbx-shim.sh`. Done once the change is merged in kixelated/dotfiles and `bin/install-dev-shell.py` has run on every host that has the hook diff --git a/quest/m0/user-dev-shell/repo-config.md b/quest/m0/user-dev-shell/repo-config.md index 4bd11ac0..3a2f23cb 100644 --- a/quest/m0/user-dev-shell/repo-config.md +++ b/quest/m0/user-dev-shell/repo-config.md @@ -28,8 +28,4 @@ Decided 2026-11-03 (maintainer, /quest-unblock); see the ## Waiting -- Maintainer: pull kixelated/dotfiles and run `bin/install-dev-shell.py` (or the full `install`, with `codex` on `PATH`) on `nixos` and `kixtop`; it changes the live agent config on each host, which an agent here doesn't touch unasked. The repositories' hooks must not go before the user-level ones arrive. [kixelated/dotfiles#33](https://github.com/kixelated/dotfiles/pull/33) is merged. On 2026-11-03, `desktop` and `nixos2` had the hook in `~/.claude-personal/settings.json`, but `nixos` and `kixtop` didn't. Check on each host that `~/.claude-personal/settings.json` has the dev-shell `SessionStart` hook and that a fresh worktree's session resolves `just` - -## Required - -- [The dev-shell hook allows only trusted, reviewed checkouts](/quest/m0/user-dev-shell/allow-rule.md) - its allow rule decides which sessions load the dev shell without a manual `direnv allow`, so it settles before the repositories drop their fallbacks +- Maintainer: pull kixelated/dotfiles and run `bin/install-dev-shell.py` (or the full `install`, with `codex` on `PATH`) on `nixos` (`kixtop` has neither direnv nor Nix, so the hook has nothing to load there; it got only the effort defaults on 2026-11-03); it changes the live agent config on each host, which an agent here doesn't touch unasked. The repositories' hooks must not go before the user-level ones arrive. [kixelated/dotfiles#33](https://github.com/kixelated/dotfiles/pull/33) is merged. On 2026-11-03, `desktop` and `nixos2` had the hook in `~/.claude-personal/settings.json`, but `nixos` and `kixtop` didn't. Check on each host that `~/.claude-personal/settings.json` has the dev-shell `SessionStart` hook and that a fresh worktree's session resolves `just` From 06d08e40d8a0fa4b2fff391bae21844751123dde Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 14:24:32 -0800 Subject: [PATCH 5/5] quest: match only Quest's own hooks for removal, and skip kixtop's check Co-Authored-By: Claude Opus 5.5 --- quest/m0/user-dev-shell/init-hooks.md | 10 +++++++--- quest/m0/user-dev-shell/repo-config.md | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/quest/m0/user-dev-shell/init-hooks.md b/quest/m0/user-dev-shell/init-hooks.md index d2b78401..e93d5158 100644 --- a/quest/m0/user-dev-shell/init-hooks.md +++ b/quest/m0/user-dev-shell/init-hooks.md @@ -18,8 +18,11 @@ to every contributor, so Quest's hooks become user-level opt-in; see the - Replaces the wrapped-hooks quest (moq-dev/moq, 2026-11-03), which widened `isQuestHook` (`src/core/init.ts`) so `init` would accept wrapped hooks. - Keep its matching rule for removal: a command hook whose last two words - are `worktree hook` is Quest's. + For removal, a command hook is Quest's only when it runs Quest: the plain + `quest worktree hook`, or a wrapper whose command ends in `worktree hook` + and names the `quest` executable or package as a word or flake attribute, + such as moq's `nix run "${CLAUDE_PROJECT_DIR}#quest" -- worktree hook`. + Another tool's command that merely ends in `worktree hook` stays. - `HOOK_EVENTS` and the hook plan in `src/core/init.ts` stop adding entries; `init` reports each entry it removes. The enrollment setup PR ([moq-dev/quest#431](https://github.com/moq-dev/quest/pull/431)) uses the @@ -35,4 +38,5 @@ to every contributor, so Quest's hooks become user-level opt-in; see the opt-in entries the docs describe. Any removal through the enrollment setup PR shows in that PR's diff. - Test `init` and `uninstall` on a repository with plain and wrapped - entries, and with other hooks beside them, which stay. + entries, and with other hooks beside them, which stay, including another + tool's command that ends in `worktree hook`. diff --git a/quest/m0/user-dev-shell/repo-config.md b/quest/m0/user-dev-shell/repo-config.md index 3a2f23cb..19bceb61 100644 --- a/quest/m0/user-dev-shell/repo-config.md +++ b/quest/m0/user-dev-shell/repo-config.md @@ -28,4 +28,4 @@ Decided 2026-11-03 (maintainer, /quest-unblock); see the ## Waiting -- Maintainer: pull kixelated/dotfiles and run `bin/install-dev-shell.py` (or the full `install`, with `codex` on `PATH`) on `nixos` (`kixtop` has neither direnv nor Nix, so the hook has nothing to load there; it got only the effort defaults on 2026-11-03); it changes the live agent config on each host, which an agent here doesn't touch unasked. The repositories' hooks must not go before the user-level ones arrive. [kixelated/dotfiles#33](https://github.com/kixelated/dotfiles/pull/33) is merged. On 2026-11-03, `desktop` and `nixos2` had the hook in `~/.claude-personal/settings.json`, but `nixos` and `kixtop` didn't. Check on each host that `~/.claude-personal/settings.json` has the dev-shell `SessionStart` hook and that a fresh worktree's session resolves `just` +- Maintainer: pull kixelated/dotfiles and run `bin/install-dev-shell.py` (or the full `install`, with `codex` on `PATH`) on `nixos` (`kixtop` has neither direnv nor Nix, so the hook has nothing to load there; it got only the effort defaults on 2026-11-03); it changes the live agent config on each host, which an agent here doesn't touch unasked. The repositories' hooks must not go before the user-level ones arrive. [kixelated/dotfiles#33](https://github.com/kixelated/dotfiles/pull/33) is merged. On 2026-11-03, `desktop` and `nixos2` had the hook in `~/.claude-personal/settings.json`, but `nixos` and `kixtop` didn't. Check on each host with direnv and Nix (`desktop`, `nixos2`, `nixos`) that `~/.claude-personal/settings.json` has the dev-shell `SessionStart` hook and that a fresh worktree's session resolves `just`; on `kixtop` the hook is an expected no-op