From 57e66b753ed3be62854e84045082407ca582e498 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 10:56:22 -0800 Subject: [PATCH 1/3] chore(quest): claim quest/m2/hook-path-error.md Quest-Claim: b5ddfaa3ac50870b From 7e42174bce83a70ea750d937984bc0d605688501 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 11:00:51 -0800 Subject: [PATCH 2/3] fix(hooks): name the launch route when quest is missing from a hook's PATH The documented worktree hook entries ran a bare `quest worktree hook`, so a Claude Code session without quest on its hook PATH failed with the shell's `quest: not found` and no guidance. Each entry now checks for quest first and otherwise prints how to fix it, naming `nix develop --command claude`, and exits 1. Exit 1 rather than 2: Claude Code keeps a subagent running on exit 2 from SubagentStop, so a missing quest would loop it, while WorktreeCreate fails on any non-zero exit. The test runs the README's entry with and without quest on PATH. Co-Authored-By: Claude Haiku 5.5 --- README.md | 8 ++++---- quest/m2/README.md | 1 - quest/m2/hook-path-error.md | 25 ------------------------- test/worktree.test.ts | 32 +++++++++++++++++++++++++++++++- 4 files changed, 35 insertions(+), 31 deletions(-) delete mode 100644 quest/m2/hook-path-error.md diff --git a/README.md b/README.md index 8d92e632..1a439f01 100644 --- a/README.md +++ b/README.md @@ -1532,10 +1532,10 @@ entries into the file's `hooks`: ```json { "hooks": { - "WorktreeCreate": [{ "hooks": [{ "type": "command", "command": "quest worktree hook" }] }], - "WorktreeRemove": [{ "hooks": [{ "type": "command", "command": "quest worktree hook" }] }], - "SubagentStart": [{ "hooks": [{ "type": "command", "command": "quest worktree hook" }] }], - "SubagentStop": [{ "hooks": [{ "type": "command", "command": "quest worktree hook" }] }] + "WorktreeCreate": [{ "hooks": [{ "type": "command", "command": "command -v quest >/dev/null || { echo 'quest is not on the PATH Claude Code runs hooks with: run nix develop --command claude in this repository, or install quest on that PATH' >&2; exit 1; }; quest worktree hook" }] }], + "WorktreeRemove": [{ "hooks": [{ "type": "command", "command": "command -v quest >/dev/null || { echo 'quest is not on the PATH Claude Code runs hooks with: run nix develop --command claude in this repository, or install quest on that PATH' >&2; exit 1; }; quest worktree hook" }] }], + "SubagentStart": [{ "hooks": [{ "type": "command", "command": "command -v quest >/dev/null || { echo 'quest is not on the PATH Claude Code runs hooks with: run nix develop --command claude in this repository, or install quest on that PATH' >&2; exit 1; }; quest worktree hook" }] }], + "SubagentStop": [{ "hooks": [{ "type": "command", "command": "command -v quest >/dev/null || { echo 'quest is not on the PATH Claude Code runs hooks with: run nix develop --command claude in this repository, or install quest on that PATH' >&2; exit 1; }; quest worktree hook" }] }] } } ``` diff --git a/quest/m2/README.md b/quest/m2/README.md index fc36765e..393c27c5 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -18,4 +18,3 @@ additional login providers. This milestone does not gate the public release. - [ChatGPT hosted runs](/quest/m2/chatgpt-hosted.md) - conditional on provider approval and current terms - [ChatGPT approval](/quest/m2/chatgpt-waitlist.md) - resolve hosted-use approval before implementation - [Delete the legacy hold labels](/quest/m2/legacy-labels.md) - once every gated repository pins quest past #326, only `quest:active` and `quest:kix` are holds -- [Worktree hook PATH error](/quest/m2/hook-path-error.md) - a worktree hook without `quest` on `PATH` says so and names the dev-shell launch route diff --git a/quest/m2/hook-path-error.md b/quest/m2/hook-path-error.md deleted file mode 100644 index b48d42b6..00000000 --- a/quest/m2/hook-path-error.md +++ /dev/null @@ -1,25 +0,0 @@ -# [XS] Worktree hooks name the launch route when quest is missing - -## Goal - -When a Claude Code session runs Quest's worktree hooks and `quest` is not on -the `PATH` Claude Code runs hooks with, the session shows an error saying so -and how to fix it, such as launching the agent inside the project's dev shell -(`nix develop --command claude`) or installing `quest` on that `PATH`, instead -of the shell's bare `quest: command not found`. A test covers it. - -## Plan - -Decided 2026-11-04 (maintainer, /quest-plan). - -- The failure happens in the shell, before any Quest code runs, so the fix - belongs in the hook entry's command: a guard that checks for `quest` and - otherwise prints the message on stderr and exits 2, which Claude Code shows - and treats as blocking. A `WorktreeCreate` failure then aborts the worktree - with that message. -- The entries are a user-level opt-in whose JSON the README's "Claude Code - checkouts" section gives and SETUP.md points to, so the guarded entry is the - documented one. -- The test runs the documented entry with a `PATH` that lacks `quest` and - checks the exit status and message, and with `quest` present checks that - the hook runs. diff --git a/test/worktree.test.ts b/test/worktree.test.ts index f31ae43b..b7ee638d 100644 --- a/test/worktree.test.ts +++ b/test/worktree.test.ts @@ -21,7 +21,7 @@ import { type Owner, Retained, type Scopes, currentOwner, kill, killable } from import { GitHub, mergedHead } from "../src/cli/github"; import { main } from "../src/cli/main"; import { Worktrees, canonical, derivedDefaults, diskUsage } from "../src/cli/worktree"; -import { claudeHook } from "../src/cli/worktree-hook"; +import { HOOK_EVENTS, claudeHook } from "../src/cli/worktree-hook"; import { initRepository, run, template } from "./fixture"; const source = dirname(dirname(fileURLToPath(import.meta.url))); @@ -1090,4 +1090,34 @@ describe("Claude Code hooks", { timeout: 20_000 }, () => { 1, ); }); + it.skipIf(process.platform === "win32")("the documented entries say so when quest is not on PATH", () => { + const readme = readFileSync(join(source, "README.md"), "utf8"); + const json = [...readme.matchAll(/```json\n(.*?)```/gs)] + .map((match) => match[1]) + .find((text) => text.includes('"WorktreeCreate"')); + if (json === undefined) throw new Error("README has no hook settings block"); + const { hooks } = JSON.parse(json) as { hooks: Record }; + const commands = HOOK_EVENTS.map((event) => hooks[event][0].hooks[0].command); + expect(new Set(commands).size).toBe(1); + const [command] = commands; + const run = (path: string, input = "") => + spawnSync("/bin/sh", ["-c", command], { input, encoding: "utf8", env: { PATH: path } }); + const dir = (): string => { + const root = mkdtempSync(join(tmpdir(), "quest-hook-")); + roots.push(root); + return root; + }; + const missing = run(dir()); + expect(missing.status).toBe(1); + expect(missing.stdout).toBe(""); + expect(missing.stderr).toBe( + "quest is not on the PATH Claude Code runs hooks with: run nix develop --command claude in this repository, or install quest on that PATH\n", + ); + const bin = dir(); + writeFileSync(join(bin, "quest"), '#!/bin/sh\necho "quest $*"\ncat\n', { mode: 0o755 }); + const input = JSON.stringify({ hook_event_name: "WorktreeCreate", cwd: "/repo", name: "agent-1" }); + const present = run(`${bin}:/bin:/usr/bin`, input); + expect(present.status).toBe(0); + expect(present.stdout).toBe(`quest worktree hook\n${input}`); + }); }); From 2d7e544a1805627ada4875b6fbb138ce3153f064 Mon Sep 17 00:00:00 2001 From: quest fixer Date: Sat, 7 Nov 2026 13:12:21 -0800 Subject: [PATCH 3/3] test: preserve tool PATH in documented hook fixture --- test/worktree.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/worktree.test.ts b/test/worktree.test.ts index b7ee638d..8e29b577 100644 --- a/test/worktree.test.ts +++ b/test/worktree.test.ts @@ -1116,7 +1116,7 @@ describe("Claude Code hooks", { timeout: 20_000 }, () => { const bin = dir(); writeFileSync(join(bin, "quest"), '#!/bin/sh\necho "quest $*"\ncat\n', { mode: 0o755 }); const input = JSON.stringify({ hook_event_name: "WorktreeCreate", cwd: "/repo", name: "agent-1" }); - const present = run(`${bin}:/bin:/usr/bin`, input); + const present = run(`${bin}:${process.env.PATH}`, input); expect(present.status).toBe(0); expect(present.stdout).toBe(`quest worktree hook\n${input}`); });