Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1335,7 +1335,7 @@ The experimental `quest worktree` commands share one disk budget across this
user's repositories and release a checkout's disk by deleting its declared
scratch in place. They only operate on Git worktrees they create; use the
owning application for Codex or other managed checkouts. Claude Code's
checkouts are Quest's own once `quest init` installs its hooks (see
checkouts are Quest's own once you opt into its hooks (see
[Claude Code checkouts](#claude-code-checkouts)).

Set a shared capacity and an explicit reservation in bytes, covering the
Expand Down Expand Up @@ -1436,9 +1436,24 @@ stolen. This initial interface refuses unknown or changed checkout ownership.

### Claude Code checkouts

`quest init` adds hook entries to the repository's `.claude/settings.json` that
run `quest worktree hook` for Claude Code's `WorktreeCreate`, `WorktreeRemove`,
`SubagentStart`, and `SubagentStop` events, so `quest` must be on the `PATH`
Quest's hooks for Claude Code are an opt-in each user adds to their own
settings, `~/.claude/settings.json`, so they apply in every repository that
user opens and no repository forces them on its contributors. Merge these
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" }] }]
}
}
```

Only one hook can manage Claude's worktrees, so remove any other
`WorktreeCreate` or `WorktreeRemove` hook first. `quest` must be on the `PATH`
Claude Code runs hooks with. For `claude --bg` and agent view, that is the
environment of the shell that started the session. The hook then creates every worktree Claude makes
in the repository: for `--worktree`, `EnterWorktree`, isolated subagents and
Expand Down
25 changes: 18 additions & 7 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,26 @@ without creating symbolic links (existing linked layouts continue to work), crea
starting with `Quests: ` to the root `AGENTS.md`, or `CLAUDE.md` when only that
exists. It also appends `quest/** merge=quest` to `.gitattributes` and sets
`merge.quest.driver` in the clone's git config, so local merges resolve
concurrent edits to quest link lists. It adds hook entries running
`quest worktree hook` to `.claude/settings.json`, keeping the file's other
settings, so Quest creates and reclaims Claude Code's worktrees; Claude Code
then needs `quest` on its `PATH` to create any worktree in the repository. It
concurrent edits to quest link lists. It writes no Claude Code hooks. It
prints each path it changed and is safe to rerun. Rerun after an upgrade, it
also rewrites the unmodified stubs whose description or setup link changed and
removes the unmodified stubs of skills the new version no longer ships.

If init refuses, it names the conflicting path. Report it and ask the user how
to proceed; never delete or rename their files to get past it.

## Opt into worktree hooks

Quest can create and reclaim Claude Code's worktrees, sharing one disk budget
across the user's repositories, through hooks in the user's own Claude Code
settings rather than the repository's, since repository settings apply to
every contributor. Offer them, explaining that they then apply in every
repository the user opens in Claude Code and need `quest` on the `PATH`
Claude Code runs hooks with. With consent, merge the entries in
[Claude Code checkouts](README.md#claude-code-checkouts) into
`~/.claude/settings.json`, keeping its other settings. If it already has a
`WorktreeCreate` or `WorktreeRemove` hook, ask the user which to keep.

## Adapt the instructions

Read the root instructions with the new line in place:
Expand Down Expand Up @@ -102,11 +111,13 @@ work or `/quest-import` to bring in open GitHub issues.
1. Run `quest skill export` and follow it, so active quests become GitHub
issues. Skip this only if the user says so.
2. Run `quest uninstall`. It removes only unmodified stubs of any version, the
`Quests: ` line, Quest's hook entries, and the merge driver, and never
deletes the quest tree.
`Quests: ` line, and the merge driver, and never deletes the quest tree.
3. Remove the pin added above: the mise entry, or the flake input and dev
shell package. For an unpinned install, delete the `quest` binary the
installer placed (`~/.local/bin/quest` by default, or `quest.exe` in the
chosen Windows install directory). Remove any PATH entry you added.
chosen Windows install directory). Remove any PATH entry you added. If the
user opted into Quest's worktree hooks and Claude Code no longer finds
`quest`, remove those entries from `~/.claude/settings.json`, since without
it they stop Claude Code making worktrees.
4. Ask the user whether to delete `quest/`. Git history keeps the plans either
way.
6 changes: 3 additions & 3 deletions cloudflare/GITHUB.md
Original file line number Diff line number Diff line change
Expand Up @@ -607,12 +607,12 @@ Submitting it opens the App's setup pull request, with no runner involved
`quest init` applies to a checkout, plans it from the default branch's tree:
the skill stubs this deployment's `quest` ships (rendered into
`build/stubs.json` at build time), `quest/README.md` with the answer as its
Goal, the `Quests: ` line in the root agent instructions, the `.gitattributes`
line, and Claude Code's hook entries in `.claude/settings.json`. Heading
Goal, the `Quests: ` line in the root agent instructions, and the
`.gitattributes` line. Heading
syntax in the answer is escaped, and the planned `quest/README.md` must pass
`quest check`, so a goal that would fail it, such as one linking a missing
quest, is refused before anything is written instead of when enabling. A path init would refuse, such as a skill
directory that is not a Quest stub or invalid settings JSON, is named on the
directory that is not a Quest stub, is named on the
page and nothing is written; kix never deletes or renames a repository's
files. The plan does not follow symlinks, since a Git tree stores their targets
as data: one on a planned path is refused the same way, pointing at running
Expand Down
2 changes: 1 addition & 1 deletion cloudflare/src/github/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1462,7 +1462,7 @@ export class GitHubService {
mode: "100644" as const,
}));
const message =
"chore: set up Quest\n\nWrites what `quest init` installs: skill stubs, quest/README.md, the agent\ninstructions' Quests line, the merge driver's .gitattributes line, and\nClaude Code's worktree hooks.\n";
"chore: set up Quest\n\nWrites what `quest init` installs: skill stubs, quest/README.md, the agent\ninstructions' Quests line, and the merge driver's .gitattributes line.\n";
// A retry builds on the default branch's files, but keeps kix's earlier commit as a parent,
// so the branch only fast-forwards: a push since the check above makes it fail rather than vanish.
const commit =
Expand Down
3 changes: 1 addition & 2 deletions cloudflare/src/github/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type TreeEntry = { path: string; type: string; mode: string; sha: string

/** Whether the plan may read the file at `path`, so the service fetches it with the tree. */
export function planned(path: string): boolean {
if (["AGENTS.md", "CLAUDE.md", ".gitattributes", ".claude/settings.json"].includes(path)) return true;
if (["AGENTS.md", "CLAUDE.md", ".gitattributes"].includes(path)) return true;
return SKILL_DIRS.some((dir) => {
const rest = path.startsWith(`${dir}/`) ? path.slice(dir.length + 1).split("/") : [];
return rest.length === 2 && rest[0]!.startsWith("quest-") && rest[1] === "SKILL.md";
Expand Down Expand Up @@ -124,7 +124,6 @@ function purpose(change: InitChange): string {
if (path.endsWith("/SKILL.md")) return "a skill stub";
if (path === "quest/README.md") return "the quest tree's root, with the project's goal";
if (path === ".gitattributes") return "routes `quest/**` through the `quest` merge driver";
if (path === ".claude/settings.json") return "Claude Code's worktree hooks, which run `quest worktree hook`";
return "points agents at `quest context`";
}

Expand Down
3 changes: 1 addition & 2 deletions cloudflare/src/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,7 @@ export function QuestSetupPage(props: {
<p>
This repository has no Quest files on its default branch yet. The App opens a pull request with what{" "}
<code>quest init</code> writes: skill stubs, <code>quest/README.md</code>, one line in the agent
instructions, a <code>.gitattributes</code> line, and Claude Code's worktree hooks. Contributions
are enabled when you merge it.
instructions, and a <code>.gitattributes</code> line. Contributions are enabled when you merge it.
</p>
<form method="post" action={`${projectURL(repo.owner, repo.name)}/setup`}>
<input type="hidden" name="id" value={repo.id} />
Expand Down
4 changes: 2 additions & 2 deletions cloudflare/test/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// the setting-up state, and enabling once it merges, against the fake GitHub.
import { env } from "cloudflare:workers";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { HOOK_COMMAND, REFERENCE_LINE } from "quest/core";
import { REFERENCE_LINE } from "quest/core";
import { app } from "../src/index";
import { STUBS, planCheck, setupGoal, treeCheckout } from "../src/github/setup";
import { type Pull, type Repo, type World, fakeGitHub, intercept } from "../dev/github";
Expand Down Expand Up @@ -115,7 +115,7 @@ describe("the setup pull request", () => {
expect(files.get("quest/README.md")).toBe("# Quests\n\n## Goal\n\nShip a fast parser.\n\\# Not a heading\n");
expect(files.get("AGENTS.md")).toBe(`# Agents\n\nRun the tests.\n\n${REFERENCE_LINE}\n`);
expect(files.get(".gitattributes")).toBe("quest/** merge=quest\n");
expect(files.get(".claude/settings.json")).toContain(HOOK_COMMAND);
expect(files.has(".claude/settings.json")).toBe(false);

expect(await setup()).toMatchObject({ state: "open", pull: pull!.number, enrolledBy: user, pending: null });
expect(await enrolled()).toBeNull();
Expand Down
14 changes: 7 additions & 7 deletions quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ ownership-manifest design:

- The binary carries the skills, the quest contract, and all hook logic.
`quest init` installs only static stubs into a repository: one-line skill
stubs, one reference line in its root instructions, one `.gitattributes`
line, and Claude hook entries in `.claude/settings.json` that only call a
`quest` subcommand. Upgrading never rewrites repository files. (Amended
2026-10-26 (user), replacing "Nothing is vendored", so `quest init` can
install the agent lifecycle hooks. Amended 2026-11-01 (user, /quest-plan):
`quest init` also rewrites a Quest-written stub nobody edited when its
text is out of date.)
stubs, one reference line in its root instructions, and one
`.gitattributes` line. Upgrading never rewrites repository files. (Amended
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).)
- 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.
Expand Down
9 changes: 5 additions & 4 deletions quest/m0/session-checkouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ Evidence, gathered 2026-11-02:
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 anymore
([quest init writes no hooks](/quest/m0/user-dev-shell/init-hooks.md)),
so this quest no longer installs them through `init`.
setup: the JSON block in the README's "Claude Code checkouts" section, which
SETUP.md's "Opt into worktree hooks" step points to, and `HOOK_EVENTS` in
`src/cli/worktree-hook.ts`. Amended 2026-11-03 (maintainer, /quest-unblock):
`quest init` writes no hook entries anymore, 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`
Expand Down
3 changes: 1 addition & 2 deletions quest/m0/user-dev-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,4 @@ 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
- [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
42 changes: 0 additions & 42 deletions quest/m0/user-dev-shell/init-hooks.md

This file was deleted.

8 changes: 6 additions & 2 deletions quest/m0/user-dev-shell/repo-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
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
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.

Expand All @@ -15,6 +15,10 @@ repository.
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
Expand Down
4 changes: 2 additions & 2 deletions quest/m0/worktree-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ agent can't do, so its README description comes from Claude Code's own code:
since it records no Git root for one, so quest's lock stays in place.

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
opt-in user-level setup the README's "Claude Code checkouts" section
describes), 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
Expand Down
11 changes: 3 additions & 8 deletions quest/m2/hook-path-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,9 @@ Decided 2026-11-04 (maintainer, /quest-plan).
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.
- After [init-hooks](/quest/m0/user-dev-shell/init-hooks.md), the entries are
a user-level opt-in that the README and SETUP.md describe, so the guarded
entry is the documented one. `quest uninstall`'s match for Quest's own
hooks must still recognize it.
- 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.

## Required

- [quest init writes no hooks](/quest/m0/user-dev-shell/init-hooks.md) - the documented user-level hook entries this guard changes
11 changes: 4 additions & 7 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,10 +846,9 @@ export function main(argv: string[], out: Output): number | Promise<number> {
.command("hook")
.summary("Handle a Claude Code worktree or subagent hook event read on stdin")
.description(
"Handle the Claude Code hook event whose JSON input arrives on stdin; `quest init` installs it in " +
".claude/settings.json for " +
"Handle the Claude Code hook event whose JSON input arrives on stdin, for " +
HOOK_EVENTS.join(", ") +
".\n\n" +
". Add it to your own Claude Code settings for those events to opt in, as the README describes.\n\n" +
"WorktreeCreate checks out the remote's default branch as last fetched (origin/HEAD), as Claude does by " +
"default, or the session's commit without one, as a quest worktree under the main checkout's .worktrees/, " +
"initializes its submodules recursively, and prints its path. A name pr-<number>, which Claude gives the worktree for --worktree '#<number>' or a " +
Expand Down Expand Up @@ -1232,17 +1231,15 @@ export function main(argv: string[], out: Output): number | Promise<number> {

program
.command("init")
.description(
"Install skill stubs, a quest root, agent pointers, Claude Code worktree hooks, and the merge driver for this repository",
)
.description("Install skill stubs, a quest root, agent pointers, and the merge driver for this repository")
.action(() => {
const changes = init(root());
for (const line of theme ? theme.init(changes) : changes) print(line);
});

program
.command("uninstall")
.description("Remove Quest stubs, markers, hooks, and the merge driver installed by `quest init`")
.description("Remove Quest stubs, markers, and the merge driver installed by `quest init`")
.action(() => {
const changes = uninstall(root());
for (const line of theme ? theme.uninstall(changes) : changes) print(line);
Expand Down
Loading
Loading