From 5fd9a42087cec25d8a09937491fc52030f846346 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 10:55:52 -0800 Subject: [PATCH 1/4] chore(quest): claim quest/m0/user-dev-shell/repo-config.md Quest-Claim: 735bdfecd49315b5 From aa679e28330530eeb772c18e22c8dfd455d05616 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 10:58:57 -0800 Subject: [PATCH 2/4] chore: drop the repository's direnv SessionStart hook Agents load the dev shell through the maintainer's user-level hooks. Co-Authored-By: Claude Opus 5.5 --- .claude/hooks/direnv.sh | 92 -------------------------- .claude/hooks/direnv.test.sh | 67 ------------------- .claude/settings.json | 15 ----- CONTRIBUTING.md | 3 +- justfile | 9 ++- quest/m0/user-dev-shell/README.md | 1 - quest/m0/user-dev-shell/repo-config.md | 38 ----------- 7 files changed, 5 insertions(+), 220 deletions(-) delete mode 100644 .claude/hooks/direnv.sh delete mode 100644 .claude/hooks/direnv.test.sh delete mode 100644 .claude/settings.json delete mode 100644 quest/m0/user-dev-shell/repo-config.md diff --git a/.claude/hooks/direnv.sh b/.claude/hooks/direnv.sh deleted file mode 100644 index efa3a378..00000000 --- a/.claude/hooks/direnv.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env bash -# Load the project's direnv/nix dev shell into the Claude Code session so Bash -# tool commands resolve flake-pinned tools (node, just, nixfmt, ...) instead of system -# ones. No-op for anyone without direnv or an .envrc, so non-nix setups are -# unaffected. -# -# Claude Code inlines $CLAUDE_ENV_FILE into the command string of every Bash -# call, and Linux caps a single argv entry at MAX_ARG_STRLEN, 128KB. Large -# development environments go in a sidecar file and -# $CLAUDE_ENV_FILE gets only a line sourcing it. macOS has no per-argument cap, -# which is why inlining the whole environment appeared to work there. - -set -eu - -MARKER='# quest dev shell' - -# Nothing to export into without this; older Claude Code versions won't set it. -[ -n "${CLAUDE_ENV_FILE:-}" ] || exit 0 - -command -v direnv >/dev/null 2>&1 || exit 0 - -# Require an explicit project dir so we never approve/export a stray .envrc from -# some unrelated working directory. -[ -n "${CLAUDE_PROJECT_DIR:-}" ] || exit 0 -cd "$CLAUDE_PROJECT_DIR" || exit 0 -[ -f .envrc ] || exit 0 - -# Clear any inherited direnv state so the dev shell is recomputed in full. -# Without this, a stale DIRENV_DIFF from the parent process makes direnv assume -# the env is already loaded and emit nothing. -unset DIRENV_DIR DIRENV_DIFF DIRENV_WATCHES DIRENV_FILE DIRENV_LAYOUT - -raw=$(mktemp) || exit 1 -scratch='' -trap 'rm -f "$raw"; [ -z "$scratch" ] || rm -f "$scratch"' EXIT - -# Clear inherited variables before restoring the snapshot, including variables -# that .envrc removed. Otherwise later shells inherit those removed values. -env -0 >"$raw" -inherited=() -while IFS= read -r -d '' entry; do - name=${entry%%=*} - [[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue - case $name in - DIRENV_* | PWD | OLDPWD | SHLVL | _ | SHELLOPTS | BASHOPTS | BASH_VERSINFO | UID | EUID | PPID) continue ;; - esac - inherited+=("$name") -done <"$raw" - -# Read the environment with `env -0` rather than a shell builtin: nixpkgs builds -# the non-interactive `bash` without programmable completion, so `compgen` does -# not exist inside the very dev shell this hook is here to load. -if ! direnv exec . env -0 >"$raw"; then - echo "direnv hook: could not load the dev shell; approve .envrc with direnv allow first." >&2 - exit 1 -fi - -if [ ! -s "$raw" ]; then - echo "direnv hook: the dev shell exported nothing, so no tools would resolve." >&2 - exit 1 -fi - -sidecar=$CLAUDE_ENV_FILE.direnv -# mktemp keeps credentials private and lets readers retain the old snapshot -# until the replacement has been written successfully. -scratch=$(mktemp "$sidecar.XXXXXX") - -{ - for name in "${inherited[@]}"; do - printf 'unset %s\n' "$name" - done - while IFS= read -r -d '' entry; do - name=${entry%%=*} - - # Skip exported functions (BASH_FUNC_foo%%) and anything else that is not a - # plain identifier, since `export` cannot restore those. - [[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue - - # direnv's bookkeeping, and the shell's own state, are not ours to restore. - case $name in - DIRENV_* | PWD | OLDPWD | SHLVL | _ | SHELLOPTS | BASHOPTS | BASH_VERSINFO | UID | EUID | PPID) continue ;; - esac - - printf 'export %s=%q\n' "$name" "${entry#*=}" - done <"$raw" -} >"$scratch" - -mv -f "$scratch" "$sidecar" - -# Refresh on every SessionStart, but install the sourcing line only once. -grep -qxF "$MARKER" "$CLAUDE_ENV_FILE" 2>/dev/null && exit 0 -printf '%s\n. %q\n' "$MARKER" "$sidecar" >>"$CLAUDE_ENV_FILE" diff --git a/.claude/hooks/direnv.test.sh b/.claude/hooks/direnv.test.sh deleted file mode 100644 index 67ce6b05..00000000 --- a/.claude/hooks/direnv.test.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -hook=$(cd "$(dirname "$0")" && pwd)/direnv.sh -fixture=$(mktemp -d) -trap 'rm -rf "$fixture"' EXIT -mkdir "$fixture/bin" "$fixture/project" -touch "$fixture/project/.envrc" -cat >"$fixture/bin/direnv" <<'MOCK' -#!/usr/bin/env bash -set -eu -[[ $1 == allow ]] && exit 0 -[[ ${FAIL_CAPTURE:-0} == 0 ]] || exit 1 -printf 'VALUE=%s\0' "$SNAPSHOT" -if [[ ${LARGE:-0} == 1 ]]; then - for ((i = 0; i < 4000; i++)); do - printf 'ITEM_%s=abcdefghijklmnopqrstuvwxyz0123456789\0' "$i" - done -fi -MOCK -chmod +x "$fixture/bin/direnv" -export PATH="$fixture/bin:$PATH" -export CLAUDE_PROJECT_DIR="$fixture/project" -export CLAUDE_ENV_FILE="$fixture/session env" -export SNAPSHOT='first value' -export REMOVED_BY_DIRENV=present -umask 022 -bash "$hook" -[[ $(bash -c '. "$CLAUDE_ENV_FILE"; printf "%s" "$VALUE"') == "$SNAPSHOT" ]] -bash -c '. "$CLAUDE_ENV_FILE"; [[ ${REMOVED_BY_DIRENV+x} != x ]]' -mode=$(ls -l "$CLAUDE_ENV_FILE.direnv") -[[ $mode == -rw-------* ]] -cp "$CLAUDE_ENV_FILE" "$fixture/original" -export SNAPSHOT="second value with \$quotes and a -newline" -bash "$hook" -cmp "$CLAUDE_ENV_FILE" "$fixture/original" -[[ $(bash -c '. "$CLAUDE_ENV_FILE"; printf "%s" "$VALUE"') == "$SNAPSHOT" ]] -cp "$CLAUDE_ENV_FILE.direnv" "$fixture/snapshot" -if FAIL_CAPTURE=1 bash "$hook"; then - echo 'failed capture was accepted' >&2 - exit 1 -fi -cmp "$CLAUDE_ENV_FILE.direnv" "$fixture/snapshot" -cat >"$fixture/fail-write" <<'MOCK' -printf() { - if [[ $1 == 'export %s=%q\n' ]]; then - return 1 - fi - builtin printf "$@" -} -MOCK -if BASH_ENV="$fixture/fail-write" bash "$hook"; then - echo 'failed write was accepted' >&2 - exit 1 -fi -cmp "$CLAUDE_ENV_FILE.direnv" "$fixture/snapshot" -LARGE=1 bash "$hook" -[[ $(wc -c <"$CLAUDE_ENV_FILE.direnv") -gt 131072 ]] -[[ $(wc -c <"$CLAUDE_ENV_FILE") -lt 1024 ]] -bash -c "$(cat "$CLAUDE_ENV_FILE")" -( - export SHELLOPTS - bash "$hook" - bash -ec '. "$CLAUDE_ENV_FILE"; [[ ${REMOVED_BY_DIRENV+x} != x ]]' -) -echo 'direnv hook: regression tests passed' diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 11a97496..00000000 --- a/.claude/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/claude-code-settings.json", - "hooks": { - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "bash \"${CLAUDE_PROJECT_DIR}/.claude/hooks/direnv.sh\"" - } - ] - } - ] - } -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 550eb64e..3ee1cd74 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,8 +15,7 @@ just build ``` For automatic shell loading, install direnv and approve this checkout with -`direnv allow`. The Claude SessionStart hook loads an already-approved environment; -it does not grant approval. No background garbage collection runs on entry. +`direnv allow`. No background garbage collection runs on entry. `flake.lock` pins Bun and the developer tools; `bun.lock` pins the dependencies. `packageManager` in `package.json` names the same Bun for CI diff --git a/justfile b/justfile index e6fdc4c2..1fd6609e 100644 --- a/justfile +++ b/justfile @@ -20,18 +20,17 @@ _check: bun run --silent quest check bun run --silent quest --root examples/export check actionlint - shellcheck .claude/hooks/*.sh scripts/*.sh - shfmt -d .claude/hooks/*.sh scripts/*.sh + shellcheck scripts/*.sh + shfmt -d scripts/*.sh nixfmt --check flake.nix nix/*.nix just worker-check -# Run tests, including the shell hooks and the lifecycle of both builds. +# Run tests, including the install scripts and the lifecycle of both builds. test: {{gate}} _test _test: bun run test - bash .claude/hooks/direnv.test.sh bash scripts/install-check.test.sh bash scripts/installer.test.sh bun run build @@ -55,7 +54,7 @@ runner-sandbox *args: # Apply formatters without changing program behavior. fix: bun run fix - shfmt -w .claude/hooks/*.sh scripts/*.sh + shfmt -w scripts/*.sh nixfmt flake.nix nix/*.nix bun run --cwd cloudflare fix diff --git a/quest/m0/user-dev-shell/README.md b/quest/m0/user-dev-shell/README.md index 10b84cad..21b135e9 100644 --- a/quest/m0/user-dev-shell/README.md +++ b/quest/m0/user-dev-shell/README.md @@ -74,4 +74,3 @@ moq-dev/moq's `.codex/hooks/direnv.sh`. ## Required - [.worktrees/ stays out of git status](/quest/m0/user-dev-shell/worktrees-exclude.md) - `quest worktree` adds `/.worktrees/` to the clone's local exclude unless something ignores it -- [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 and `nix develop` instructions diff --git a/quest/m0/user-dev-shell/repo-config.md b/quest/m0/user-dev-shell/repo-config.md deleted file mode 100644 index 7a82a04c..00000000 --- a/quest/m0/user-dev-shell/repo-config.md +++ /dev/null @@ -1,38 +0,0 @@ -# [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, 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). - -- moq's four Quest worktree hook entries are not this quest's: decided - 2026-11-04 (maintainer, /quest-complete), - [moq-dev/moq#7889](https://github.com/moq-dev/moq/pull/7889) removes them - on its own. -- 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`. -- User-level hooks are on every host with direnv and Nix (2026-11-04, - /quest-unblock): `desktop`, `nixos2` and `nixos` have the dev-shell - `SessionStart` hook in `~/.claude-personal/settings.json`. On `nixos` the - maintainer chose to discard the checkout's local edits; they are kept as - branch `backup/nixos-local-2026-11-04` and - `.git/nixos-local-edits-2026-11-04.patch` there. `kixtop` has neither - direnv nor Nix, so the hook is a no-op there. From 492b474e69d146adf061e2d631e5926373dc4fe4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 21:22:30 -0800 Subject: [PATCH 3/4] chore(quest): claim quest/m0/user-dev-shell/repo-config.md Quest-Claim: afbab5fe22bb310b From e2556b81dca626a8f75bfb00823a9572f1078ed3 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 21:40:43 -0800 Subject: [PATCH 4/4] quest: finish user-dev-shell The user-level dev-shell-hook now checks out uninitialized submodules the flake uses (kixelated/dotfiles#52), and moq.pro's PRs drop its remaining Codex and Claude dev-shell config. Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 3 +- quest/m0/user-dev-shell/README.md | 72 -------------------------- quest/m0/user-dev-shell/repo-config.md | 35 ------------- 3 files changed, 1 insertion(+), 109 deletions(-) delete mode 100644 quest/m0/user-dev-shell/README.md delete mode 100644 quest/m0/user-dev-shell/repo-config.md diff --git a/quest/m0/README.md b/quest/m0/README.md index c6f66c69..b5aeddeb 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -81,8 +81,7 @@ ownership-manifest design: 2026-11-01 (user, /quest-plan): `quest init` also rewrites a Quest-written stub nobody edited when its text is out of date. Amended 2026-11-03 (maintainer, /quest-unblock): `quest init` writes no Claude hook entries; - Quest's worktree hooks are a user-level opt-in, see - [user-dev-shell](/quest/m0/user-dev-shell/README.md).) + Quest's worktree hooks are a user-level opt-in.) - The repository's own tool manager pins the version: mise (release binaries) or a nix flake input (any rev, built from source). Quest ships no launcher and no pin file; an install outside either is simply unpinned. diff --git a/quest/m0/user-dev-shell/README.md b/quest/m0/user-dev-shell/README.md deleted file mode 100644 index 360d8d0e..00000000 --- a/quest/m0/user-dev-shell/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# 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 - 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. - 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 - 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`. diff --git a/quest/m0/user-dev-shell/repo-config.md b/quest/m0/user-dev-shell/repo-config.md deleted file mode 100644 index 65e570fc..00000000 --- a/quest/m0/user-dev-shell/repo-config.md +++ /dev/null @@ -1,35 +0,0 @@ -# [S] moq.pro drops its Codex dev-shell config - -## Goal - -moq-dev/moq.pro drops its repo-level Codex dev-shell config, the same as -quest, moq, rind, and rust2 did: `.codex/hooks.json`'s `nix-devshell.sh` -`SessionStart` hook and `pretooluse-devshell.py` `PreToolUse` hook, -`.codex/config.toml`'s repo-local `BASH_ENV`/`ENV`/`ZDOTDIR`, the -`.codex/shell/` files, and their tests and `check.sh` routes. Codex there -still runs the pinned tools, through the maintainer's user-level hooks. - -## Plan - -Decided 2026-11-07 (maintainer): moq.pro drops all of its dev-shell config. -Its Claude direnv hook is already gone. Its Codex config stays until the -Goal's check passes. - -- Keep `.claude/hooks/submodule.sh`, `worktree.sh`, `tofu-secrets.py`, and - `.codex/environments/environment.toml`'s submodule setup. -- Check: in a fresh moq.pro clone with `.codex/` removed and a clean - environment, `codex exec` resolves `which just`. - -Decided 2026-11-08 (maintainer, /quest-unblock): a fresh headless Codex -session in moq.pro couldn't load the dev shell without the repo hooks, since -the flake takes `path:./moq` and nothing initialized that submodule for -`codex exec`. ✅ (a) the user-level `dev-shell-hook` runs -`git submodule update --init` when `.gitmodules` lists an uninitialized path -the flake needs (rejected: (b) keep moq.pro's Codex config for good; (c) -accept a manual submodule init in fresh clones). It also fixes a cold first -Claude session, where the user-level hook ran before moq.pro's -`submodule.sh` and printed no `just`. - -- The hook lives in kixelated/dotfiles (`bin/dev-shell-hook`, documented in - `docs/dev-shell.md`), so this quest's work opens a PR there first, then - the moq.pro PR that drops the Codex config once the Goal's check passes.