Forking a session is easy; forking your files isn't. This Claude Code / Codex skill gives the forked session its own branch and worktree with every uncommitted file copied and verified.
You are deep in a Claude Code or Codex session. The agent has context you do not want to rebuild, and your working tree is full of uncommitted work. Now you want to try a second approach — without stashing, without losing the first one, and without starting the conversation over.
The agent-fork skill does that without leaving the conversation. Type
/agent-fork try-redis (Claude Code) or $agent-fork try-redis (Codex) and it
creates a new branch and a linked Git worktree, copies your current staged,
unstaged, and untracked files into it, verifies the copy matched, and prints
the exact command that continues this conversation in that new worktree. Your
original worktree and session are never touched.
Underneath the skill is a standalone agent-fork CLI that does the same work
from any terminal. Outside an agent session it prints a cd command instead of
a session-continuation command, so the same tool covers plain Git workflows.
From inside your Claude Code or Codex session:
/agent-fork try-redis # fork without leaving the conversation
Don't have the skill yet? One command installs it for both agents:
npx skills@latest add smorinlabs/agent-forkThe skill previews the fork — target branch, destination worktree, the files it would carry — and asks for confirmation before creating anything. It drives the CLI below, which works directly from a terminal too:
$ agent-fork fork try-redis --dry-run
branch: create fork/try-redis
worktree: create /Users/you/code/myapp-fork-try-redis
files-to-carry: staged=3 unstaged=7 untracked=2 ignored=0
setup-hook: none
paste command: cd /Users/you/code/myapp-fork-try-redis && claude --session-id 9b74b9f2-f3d2-4060-b233-0121ac17ed7c --resume c854b79c-16b2-4863-a095-03d35d195ec9 --fork-session -n try-redis
validation: local-only; no mutation performedDrop --dry-run and the fork is created. Paste the final line into a fresh
terminal and the agent picks up where it left off — same context, new branch,
your uncommitted work already there.
The full set of skill invocations:
/agent-fork try-redis # explicit fork name
/agent-fork try-redis --now # fork immediately, no confirmation
/agent-fork # fork with context-aware naming
/agent-fork --session # inspect this agent session
/agent-fork --session-only # print only the native session-fork command
Codex invokes the same skill as $agent-fork; every form above is otherwise
identical.
The skill delegates operations to the CLI. It first validates a session inspection, then uses the selected runner for the operation's command:
| Skill form | CLI call | Result |
|---|---|---|
| The fork forms | agent-fork fork ... --require-agent --json |
The branch, the worktree, and the paste command |
--session |
agent-fork session --json |
The session inspection plus its transcript path and its native fork and resume commands |
--session-only |
agent-fork session --json |
Only that native fork command |
The fork's name comes from one of three places:
- A name hint you typed is normalized to lowercase kebab-case
(
"Review Auth"becomesreview-auth). - With no hint on a topic branch — an ordinary feature branch — the CLI derives the name automatically (dated, with a suffix if it collides).
- With no hint on the default branch, a detached HEAD, or an unclassifiable branch, the skill proposes a name from the conversation.
Every fork is confirmed before it exists: the skill runs a dry run, shows you
the target branch, the destination worktree, and the files it would carry,
and creates the fork only after you approve. --now skips that confirmation;
the name is still chosen the same way.
The skill uses the installed agent-fork CLI when its output supports the
requested operation. If the CLI is missing or lacks a required field, the
assistant runs the operation through uvx from the official Git repository.
It uses uv tool run when only uv is available. Fetching the tool does not
add a confirmation question; the environment's execution permissions apply.
The assistant resolves one source commit and keeps the same command prefix
for inspection, the fork preview, and an authorized fork. A temporary run
does not replace the installed CLI. You can also ask the assistant to install
or upgrade it permanently, or to use a confirmed local checkout through
uv run --project. That checkout supplies the code while the command runs
in your active repository. The skill checks the reported directory before
accepting an inspection or proceeding to a fork.
Two limits are deliberate: the skill accepts no CLI flags beyond the forms above (advanced flags are for direct CLI use), and the two inspection forms always use the active session's directory (there is no directory option).
The skill ships as one copy: the Agent Skills artifact at
.agents/skills/agent-fork in this repository. Codex discovers it there as
$agent-fork; Claude Code discovers the same files through the repository's
.claude/skills/agent-fork symlink as /agent-fork. The user-level installs
above create the same two links under your home directory. The plugin manifests (.claude-plugin/ for Claude
Code, .codex-plugin/ for Codex) and the repo-root skills/ directory all
point at that same artifact.
The skill installs with one command, for Claude Code and Codex at once:
npx skills@latest add smorinlabs/agent-forkClaude Code can install it as a plugin instead — this repository is its own marketplace:
/plugin marketplace add smorinlabs/agent-fork
/plugin install agent-fork@agent-fork
The skill can run the CLI through uvx, so a permanent installation is
optional. To make agent-fork available directly in any terminal, ask the
assistant to install it or run:
uv tool install git+https://github.com/smorinlabs/agent-forkOr inspect the current session in a temporary tool environment. --isolated
avoids an older installed CLI; --refresh refreshes the cached source:
uvx --isolated --refresh --from git+https://github.com/smorinlabs/agent-fork agent-fork session --jsonAfter PyPI publication the no-install command becomes simply
uvx agent-fork.
Requirements: Python 3.11+ and Git 2.19+. Forking an agent session
additionally needs Claude Code 2.0.73+ or Codex 0.95+ (Codex's native fork
itself requires 0.81+). Run agent-fork doctor to check all of this at once.
If you would rather not use the npx installer or the plugin, clone the repository and symlink the canonical artifact into the user-level skill directories for both agents:
git clone https://github.com/smorinlabs/agent-fork
cd agent-fork
mkdir -p ~/.claude/skills ~/.agents/skills
ln -sfn "$PWD/.agents/skills/agent-fork" ~/.claude/skills/agent-fork
ln -sfn "$PWD/.agents/skills/agent-fork" ~/.agents/skills/agent-forkClaude Code then discovers it as /agent-fork and Codex as $agent-fork.
Run these commands from the repository root. The editable uv tool install
keeps the command connected to this checkout; skill placement uses the same
two symlinks shown above.
uv tool install --editable --force .
mkdir -p ~/.claude/skills ~/.agents/skills
ln -sfn "$PWD/.agents/skills/agent-fork" ~/.claude/skills/agent-fork
ln -sfn "$PWD/.agents/skills/agent-fork" ~/.agents/skills/agent-forkVerify the installed version and both placements:
agent-fork --version
readlink ~/.claude/skills/agent-fork ~/.agents/skills/agent-forkThe version command must print agent-fork 1.2.1.
Both symlinks must resolve
to this repository's .agents/skills/agent-fork directory.
uv tool uninstall agent-fork
npx skills@latest remove agent-forkA symlink install is removed with
rm ~/.claude/skills/agent-fork ~/.agents/skills/agent-fork; a plugin install
with /plugin uninstall agent-fork.
Inside a session, just type /agent-fork — the commands below are the
direct-CLI equivalents:
agent-fork doctor # confirm Git, agent CLIs, config, XDG paths, setup hook
agent-fork fork try-redis # create the fork, print the paste command
agent-fork session # inspect context and print native fork/resume commands
agent-fork list # see the forks you have created
agent-fork cleanup try-redis --yes # remove one when you are done- Detect the agent and parent session from
CLAUDE_CODE_SESSION_IDorCODEX_THREAD_ID— or accept both explicitly as flags. - Anchor to the parent's current commit and create the fork branch.
- Create a linked Git worktree at the destination.
- Copy staged, then unstaged, then untracked files into it, preserving symlinks and the executable bit — then, per gitlink, carry each submodule the same way, recursively (see Submodules).
- Verify that the new worktree matches what was promised — Git-visible state, and the contents themselves: staged entries, file types, permissions, symlink targets, and a checksum of every carried file. The parent is snapshotted before the fork starts and rechecked afterwards, so a file that changes mid-fork fails rather than producing an ambiguous copy. A failed check rolls the fork back and reports exactly what to do.
- Emit the launch command for the detected agent, and record the fork in a
local registry so
listandcleanupcan find it later.
Why not just git worktree add? That gives you an empty worktree at HEAD.
agent-fork carries your uncommitted work across, proves the copy is faithful,
rolls back cleanly when it is not, and continues the agent conversation rather
than starting a new one.
| Command | Purpose |
|---|---|
agent-fork fork [NAME] |
Create a verified branch and worktree; print the paste command |
agent-fork session [validate] |
Inspect session evidence, construct its native fork and resume commands, or assert expected identity and lineage |
agent-fork list |
List forks created by agent-fork |
agent-fork cleanup <name|branch|worktree> --yes |
Remove a registered fork |
agent-fork prune [--dry-run] --yes |
Remove registry records that are gone or unusable; never touches a worktree |
agent-fork doctor |
Diagnose Git, agent, config, and XDG readiness, plus whether this repository's setup hook would run |
agent-fork config view|get|set|validate |
Inspect or update configuration |
agent-fork completion bash|zsh|fish |
Generate a shell completion script |
agent-fork help [command] |
Show help for a command |
Global options: -V/--version, -v/--verbose, -q/--quiet, --debug, and
--config PATH. Commands that emit formatted results (fork, list,
session, cleanup, doctor, and config view) accept
-o/--output {text,json} and default to text; --json is an alias for
-o json.
cleanup acts only on a fork the invoking repository actually has. A registry
record names a candidate; the repository's live worktree list decides whether
that record still describes reality. A record whose worktree and branch are not
a live pair here is refused with cleanup_registry_stale, whether it belongs to
a different repository or to a fork that has since been removed — and --force
does not override that refusal, because it is the only evidence that the
worktree still belongs to the repository its record names. If several records
claim one target, cleanup refuses with cleanup_registry_ambiguous rather
than choosing.
Two consequences are worth knowing. Run cleanup <name> from inside the
repository: a fork name is resolved against the invoking repository, so the
same name may legitimately exist in several repositories and outside a
repository there is nothing to resolve against. And a record left behind by a
worktree you deleted yourself is cleared with agent-fork prune, which
changes only the registry — it never removes a worktree or a branch. prune
removes a record when nothing is at its recorded path, and when the record
predates the repository field and so cannot identify its fork; a path now occupied
by something else is reported and kept, because that may be another
repository's live worktree.
cleanup is not atomic across Git's worktree removal, branch deletion, and the
registry update: an interruption can leave only some of those steps complete.
During that destructive section, SIGINT or SIGTERM kills the active Git process
group and exits 130 or 143 with a partial-removal warning. There is no
attempt to restore files Git already removed. Inspect the worktree and branch;
if the worktree is gone but its registry record remains, run
agent-fork prune.
cleanup always inspects the
target for uncommitted changes and commits that are not reachable from a remote.
Without the matching override, it refuses and lists up to 10 at-risk paths or
commits before reporting how many additional entries were omitted. Untracked
paths are grouped separately from modified paths.
--force keeps its 1.x behavior: it extends targeting beyond registered forks
and overrides both Git safety guards. --allow-dirty and --allow-unpushed
override only their named guard. None of these flags replaces consent via
--yes, and none overrides the refusal to remove the invoking working
directory. Agent-owned session files are never removed.
Use the same interface inside Claude Code or Codex:
agent-fork session
agent-fork session -o json
agent-fork session validate --agent codex --has-parentInspection reports sourced evidence, the resolved invocation directory, nullable Git repository context, and two shell-quoted native commands for the one detected agent identity:
fork_command— creates a new session: a fresh session ID, a new Git branch and worktree, carrying a copy of the current file state forward. The original session keeps running untouched.resume_command— re-enters the same session in place: same session ID, same directory, same branch/worktree, continuing the same transcript. Use this to pick a session back up after setting it aside ("rehydrate" it) — nothing new is created.
fork: cd '<resolved-directory>' && claude --session-id '<fresh-child-uuid>' --resume '<current-session-id>' --fork-session
codex fork '<current-thread-id>' -C '<resolved-directory>'
resume: cd '<resolved-directory>' && claude --resume '<current-session-id>'
codex resume '<current-thread-id>' -C '<resolved-directory>'
Inspection also reports transcript: where this session's conversation is
stored on disk.
claude: <CLAUDE_CONFIG_DIR|~/.claude>/projects/<encoded-directory>/<session-id>.jsonl
codex: <CODEX_HOME|~/.codex>/sessions/<YYYY>/<MM>/<DD>/rollout-<timestamp>-<thread-id>.jsonl
transcript.path is the absolute path and transcript.exists says whether a
file is there right now. The two agents store transcripts differently, and the
difference is visible in the field. The Claude path is derived from the
session ID and the resolved invocation directory — every non-alphanumeric
character of that directory becomes - — so it can be reported before the
file is flushed. Because the derivation keys on the directory agent-fork
itself was invoked in, running it from somewhere other than the session's
working directory derives a path that does not exist and reports
exists: false. Claude Code re-keys its transcript folder when the session's
directory changes, so an ordinary in-session invocation — including from a
linked worktree the session moved into — resolves correctly. The Codex path is
discovered by search, because the rollout filename embeds a timestamp, so a
Codex transcript.path is null whenever no rollout matches. A session ID that
is not [A-Za-z0-9-]+ never reaches the filesystem and reports a null path.
Inspection reports the location only; it never reads the transcript.
JSON inspection also reports agent_signal, an additive object with
status, present, and missing. Its status is absent, incomplete,
detected, or ambiguous. The detail lists contain supported environment
variable names, never session or thread values. An incomplete Claude signal
remains observational in session: it reports no current session, retains
lineage.status: not_detected, fork_command.status: not_detected, and
resume_command.status: not_detected, and names the missing value in
notices.
JSON inspection also reports parent_inference, an additive object that
surfaces a recorded Claude parent inference even when it is no longer fully
current. Its status is one of not_consulted, absent, current,
last_known_good, freshness_unknown, superseded, or unreadable. Only
current is strict parent evidence — parent_session and
session validate --has-parent never see anything less than current, no
matter what parent_inference reports. last_known_good and
freshness_unknown still show the previously inferred parent ID and analysis
timestamp, with a notice naming the rerun command; superseded shows only its
status. session never triggers a new inference to compute this field — it
only reads what a prior session claude-parent infer --record already wrote.
See docs/session-inspection.md for the full
status vocabulary and field shape.
Inspection never executes the returned command, mutates agent or repository
state, or makes a network call; in an ordinary terminal it succeeds with
not_detected. Validation asserts expected identity and lineage: optional
--agent, --session-id, --parent-session-id, and
--has-parent/--no-parent constraints compose with AND semantics.
Claude does not expose an authoritative historical parent ID for ordinary
forks; the explicit agent-fork session claude-parent infer analysis can infer
likely relationships from transcript structure and record them as local
evidence. The complete semantics — repository-context fields, availability
rules, parent-evidence caveats, and the inference commands, caching, and
evidence labels — are documented in
docs/session-inspection.md.
| Flag | Effect |
|---|---|
TARGET |
Select a fork by registered name, branch, or worktree path |
--force |
Allow an unregistered target and override both the dirty and unpushed guards |
--allow-dirty |
Override only the guard against uncommitted changes |
--allow-unpushed |
Override only the guard against commits absent from every remote |
--keep-branch |
Remove the worktree but preserve its branch |
--yes |
Supply non-interactive consent; it does not override safety guards |
--no-input |
Never prompt; fail unless required consent was supplied with --yes |
--dry-run |
Run the full safety inspection and preview removal without mutating |
-o/--output {text,json} |
Select the result format; default: text |
--json |
Alias for --output json |
Examples:
agent-fork cleanup review-auth --dry-run
agent-fork cleanup review-auth --allow-dirty --yes
agent-fork cleanup ../unregistered-worktree --force --dry-run
agent-fork cleanup review-auth --force --dry-run --jsonA human forced preview writes its removal plan to stdout and its at-risk warning
to stderr. A JSON guard refusal places details inside the stderr error object;
a JSON dry-run result places the same object at the top level on stdout:
{
"dirty": [
{"status": " M", "path": "tracked.txt"},
{"status": "??", "path": "important_untracked.txt"}
],
"dirty_count": 2,
"dirty_truncated": false,
"unpushed": [
{"sha": "a1b2c3d", "subject": "wip: parser rewrite"}
],
"unpushed_count": 1,
"unpushed_truncated": false
}Each array contains at most 10 entries. Its *_count field reports the complete
count, and its *_truncated field is true when additional entries were
omitted. Human diagnostics render backslashes, terminal control characters, and
undecodable path bytes as visible C-style escapes such as \\x1b; structured
JSON retains the underlying string values. Dry runs that find overridden risk
still exit 0; a dirty or unpushed guard refusal exits 5.
By default, cleanup never removes a target's planned Claude parent claim,
inferred parent record, or freshness corroboration — the forked session
remains resumable and this is agent-fork's strongest local parent evidence.
Both real and --dry-run cleanup runs disclose what is retained: an additive
retained_metadata object (lineage_claims, inferred_records,
freshness_entries, and one source-qualified removal_commands entry per
retained record) in JSON output, and matching notice: lines in human output.
Run the disclosed agent-fork session claude-parent delete --session-id <ID> --source {planned,inferred} --yes command yourself to remove a specific
record; cleanup adds no flag that prunes this metadata automatically.
| Flag | Effect |
|---|---|
NAME |
Fork identity; derived from the current branch when omitted |
--agent {claude,codex} |
Host agent; detected when omitted |
--parent-session ID_OR_NAME |
Parent session/thread UUID, or a renamed Codex session name |
--require-agent |
Refuse unless a single usable agent session is available |
--no-agent |
Ignore agent signals; create only the branch and worktree |
--branch BRANCH |
Explicit fork branch name |
--worktree-dir PATH |
Use this exact worktree destination |
--worktree-base-dir DIR |
Replace only the derived parent directory |
--worktree-name COMPONENT |
Replace only the derived directory name |
--with-state / --no-with-state |
Carry staged, unstaged, and untracked state (default: enabled) |
--with-ignored / --no-with-ignored |
Also carry ignored files (default: disabled) |
--with-submodules / --no-with-submodules |
Carry each submodule's working-tree state, recursively (default: enabled) |
--verify / --no-verify |
Verify the completed fork (default: enabled) |
--strict |
Refuse and roll back when any carried entry must be skipped |
--codex-session-name-resolution / --no-codex-session-name-resolution |
Resolve renamed Codex sessions (default: enabled) |
--setup-hook-policy {tracked,any,off} |
Repository setup-hook policy: tracked runs it only when it is committed at the fork anchor and unchanged on disk, any runs it regardless, off never runs it (default: tracked) |
--setup-hook-timeout SECONDS |
Seconds the setup hook may run before its process group is terminated (default: 300) |
--force |
Override only the Git-version floor |
--dry-run |
Preview every planned mutation without changing anything |
--copy / --no-copy |
Copy the paste command to the clipboard |
Examples:
agent-fork fork review-auth # auto-detect agent or Git-only
agent-fork fork terminal-copy --no-agent # explicitly Git-only
agent-fork fork session-copy --require-agent
agent-fork fork review-auth --with-ignored
agent-fork fork review-auth --no-with-submodules
agent-fork fork --no-with-state --dry-run
agent-fork fork --no-with-state --dry-run -o json
agent-fork fork review-auth -o json
agent-fork fork experiment --branch review/manual \
--worktree-base-dir /work/forks --worktree-name 'Manual Worktree'Dry-run JSON is a preview schema rather than a completed-fork result. It sets
dry_run: true and reports the planned branch and worktree creates, staged,
unstaged, untracked, and ignored file counts, the paste command, notices,
local validation status, and mutation_performed: false.
A fork does not fail solely because one untracked or ignored entry cannot be
copied. The entry is omitted, the successful command names it once on stderr,
and fork --json records it in both notices[] and skipped[]:
{"skipped":[{"path":"locked.txt","phase":"capture","reason":"unreadable"}]}Each skipped[] entry has the stable string fields path, reason, and
phase. Entries are ordered byte-wise by path. A skip is permitted only when
the path is untracked or ignored, its lstat metadata was observed, and the
fork carries no deletion. A tracked path, a failed lstat, or a deletion that
could be the other half of a rename fails with entry_unreadable instead.
Parent changes during the fork still fail verification and roll the fork back.
--strict changes any otherwise-permitted skip into the exit-1
strict_skip_refused error and rolls the fork back. It is an invocation-only
flag: there is no configuration key or environment variable for it.
The default auto mode detects the agent and parent session from
CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID. They can be supplied explicitly:
agent-fork fork review-auth \
--agent claude --parent-session '<session-uuid>' \
--branch review/auth --worktree-dir '../auth-review'
# A renamed Codex thread can be supplied instead of its UUID.
agent-fork fork review-auth \
--agent codex --parent-session 'hello-codex' --dry-run--agent with an explicit --parent-session works without either environment
variable and implies strict agent behavior. --require-agent refuses unless a
single usable session is available; --no-agent ignores agent signals. Set the
default with [fork] agent_mode = "auto" | "strict" | "git-only" or
AGENT_FORK_AGENT_MODE.
--worktree-dir selects one exact destination. Alternatively,
--worktree-base-dir and --worktree-name independently replace the parent and
leaf of the configured/default destination and may be combined. An explicit base
must already exist. The exact-path flag cannot be mixed with either partial
override.
Codex stores a rename separately from the rollout filename, so a name such as
hello-codex cannot be used directly with codex fork. When an explicit Codex
parent is not a UUID, agent-fork asks the installed local Codex app-server for
exact name matches, replaces the name with the canonical thread UUID, verifies
that UUID's rollout is present, and emits the normal
codex fork <uuid> -C <worktree> command. UUID input bypasses this lookup. The
lookup is local and bounded; agent-fork never reads Codex's internal SQLite
database directly.
Name resolution is enabled by default. Disable it for a strict UUID-only path
with --no-codex-session-name-resolution, or persist that choice:
[agents.codex]
session_name_resolution = falseThe corresponding config key is agents.codex.session_name_resolution. With
resolution disabled, a non-UUID Codex parent is rejected before repository
mutation. Missing and duplicate names are also rejected; duplicate diagnostics
list the candidate UUIDs. This option is intentionally Codex-specific. If
another agent later needs the same facility, a future design may add a generic
option while retaining this name as a compatible alias.
Configuration is TOML, discovered per the XDG/project precedence documented in
REQUIREMENTS.md.
--config PATH replaces discovery entirely. State — the fork registry backing
list and cleanup — lives under $XDG_STATE_HOME/agent-fork.
The registry is one file per user account, shared by every repository, and each record names the repository it belongs to. Records written by agent-fork 1.2 and earlier predate that field. They are read and preserved, not rejected, and no repository is inferred for them: the only path such a record carries is where its worktree was when the record was written, which is not evidence about what is there now.
Such a record authorizes nothing. It still appears in list, but
cleanup <name> refuses it, and forking will not fill in its repository —
matching a live worktree is not proof, because two repositories can hold the
same path on the same branch name, which the central worktree layout makes
ordinary since it keys on a repository's basename alone.
The forks themselves are unaffected. To finish an older fork:
agent-fork prune # clears the unusable records
agent-fork cleanup /path/to/the/worktree --force # removes the fork itselfprune never touches a worktree, so the order does not matter. Note that once
agent-fork 1.3 writes the registry, earlier versions will not read it.
[fork] key |
Default | Environment variable | Notes |
|---|---|---|---|
with_state |
true |
— | Carry staged, unstaged, and untracked files |
with_ignored |
false |
— | Also carry ignored files; implies with_state |
with_submodules |
true |
— | Carry each submodule's state recursively; does not imply with_state |
branch_prefix |
"fork/" |
— | Whitespace falls back to the default; must compose into a valid Git branch name |
worktree_location |
"sibling" |
— | sibling, central (XDG data), subdirectory, or a path template — see below |
agent_mode |
"auto" |
AGENT_FORK_AGENT_MODE |
auto, strict, or git-only |
verify |
true |
— | Run the verification ladder |
copy |
false |
— | Copy the paste command to the clipboard |
output |
"text" |
AGENT_FORK_OUTPUT |
text or json, for every command that resolves configuration |
setup_hook_policy |
"tracked" |
— | tracked, any, or off; see "Repository hooks" |
setup_hook_timeout |
300 |
— | Whole seconds, greater than zero; there is no "no timeout" value |
A path-template worktree_location may use only the bare placeholders
{repo-name}, {repo-root} (the repo root's parent directory), {branch},
and {branch-escaped} — no format specifier, conversion, or subscript (e.g.
{repo-name:>10}, {repo-name!r}, {repo-root[0]}), and the template must
render to an absolute path (start with /, ~, or {repo-root}). A ~user
form is rejected; a bare ~/ is accepted. {session-id} is not a supported
placeholder — no session ID exists yet when the destination is derived, and
none is planned to be added.
If config validate (or doctor, or a dry run) rejects a value, the message
names the exact key, the value, the allowed forms, and which of TOML/environment/flag
supplied the winning value.
Per-agent tables append arguments to the emitted command, each element individually shell-quoted:
[agents.claude]
extra_args = []
[agents.codex]
extra_args = []
session_name_resolution = trueAGENT_FORK_CONFIG selects a config file, equivalent to --config.
AGENT_FORK_OUTPUT selects text or json for commands that resolve effective
configuration and defaults to text; it is equivalent to the output key
above and can also be set via config set output <text|json>.
Every effective key is addressable with config get/config set, using
either the bare [fork] name (e.g. branch_prefix) or a fully dotted form
(e.g. fork.branch_prefix, agents.claude.extra_args,
agents.codex.session_name_resolution). The two extra_args arrays are
readable but not settable from the CLI — config set on either refuses with
the exact TOML file and table to hand-edit instead; general array-editing
syntax is out of scope.
An explicit flag beats config and suppresses dependent config settings — a
config with_ignored = true combined with --no-with-state carries no state.
.worktreeinclude may list ignored files to copy after verification.
An unreadable matching file follows the same skip policy and appears with
phase: "include". Include copying deliberately remains after verification,
and copied include files do not have a per-copy stability bracket. A file
changed during copying can therefore produce stale or torn child content
without detection. Closing that accepted limit requires a separate
copy-bracketing design.
An optional .agent-fork/worktree-setup.sh runs non-fatally in the new worktree
with REPO_ROOT and WORKTREE_PATH set. The hook lives in the repository being
forked, not in agent-fork, so the repository supplies its content and
agent-fork only supplies the execution mechanism — the same division as npm
and a package's postinstall script.
Which hooks run. --setup-hook-policy (or [fork] setup_hook_policy)
decides:
| Policy | What runs |
|---|---|
tracked (default) |
Only a hook committed in the fork anchor's tree as a regular file whose bytes on disk are identical to the committed blob |
any |
The hook runs whatever its provenance; the observed eligibility is still reported, never masked |
off |
Nothing runs and eligibility is never evaluated |
Eligibility is evaluated in the new worktree, immediately before execution,
because agent-fork carries your uncommitted state across: checking the copy
that will actually execute is the only version of this check that means
anything. A hook that fails the check is skipped, not refused — the fork
still succeeds, with a named reason and the exact override to run it anyway.
tracked is a provenance control, not a safety one. It stops a cloned
repository from silently running new, unreviewed code on your next fork. A
committed hook is one a reviewer could have read; it is not one that is safe,
and a malicious committed hook runs exactly as before.
Bounds. The hook runs in its own process group with stdin connected to
/dev/null, and gets --setup-hook-timeout seconds (default 300) before that
whole group is terminated. The group is the exact unit of the guarantee: a
timeout, or an interrupt, kills the hook and every process it started that
stayed in the group. A process that calls setsid() puts itself in a
different group, where killpg cannot reach it — that is how Unix works, not
something agent-fork can override.
What is guaranteed for such an escaped process is that it cannot make your fork
hang. agent-fork waits a bounded moment for the hook's output to close, then
lets go, reports the hook's own exit code, and sets descendants_cleared to
false in --json with a matching notice, so a process left running is stated
rather than hidden. On the success path nothing is killed at all: a hook that
deliberately starts a background process keeps it.
A timeout bounds how long the hook runs; it cannot undo work the hook already
did. A hook killed at the timeout may already have pushed a branch, written
~/.npmrc, or changed shared repository configuration, and none of that is
rolled back. The /dev/null stdin is a behavior change from earlier versions:
a hook that used to prompt interactively now reads end-of-file instead of your
terminal.
Visibility. fork narrates the hook on stderr in human output and carries a
setup_hook object in --json, including a bounded tail of the hook's own
stdout and stderr whether it succeeded or failed. fork --dry-run and doctor
both disclose the hook before anything runs. doctor evaluates against HEAD
in the current worktree while fork resolves its own anchor; on a detached
HEAD those can differ, which is why doctor's message names HEAD
explicitly.
Note that this makes doctor able to fail on your repository's working-tree
state, not only on machine readiness: a hook that is present but ineligible
under the default tracked policy is reported as a failure, because it will
silently not run. It stays passing under any (the hook is explicitly allowed
to run), under off (it is not evaluated), and when no hook is present.
- Your parent worktree and session transcript are never modified. The fork is additive.
- Verification is on by default, and it compares contents. Matching
git statusoutput is not enough: the fork's carried files are compared by checksum, along with staged entries, file types, permissions, and symlink targets. Configuration that would silently rewrite content in transit — such asapply.whitespace, end-of-line conversion, or content filters — is therefore caught rather than reported as a successful copy. A failed check rolls the fork back and reports the exact manual recovery steps. - Ignored files stay put unless you ask.
--with-ignoredmay copy secret-bearing files such as.envbetween working trees, which is precisely why it is off by default. - Skipped entries are named and bracketed. A final metadata sentinel catches
a skipped target that changes before publication. The sentinel covers the
target, not every ancestor: an ancestor permission or rename race between
lstatand the later read can be reported as an unreadable skip even when the target itself was readable at the fork boundaries. The omission is named; closing this accepted limit requires descriptor-based traversal. cleanupis deliberately hard to misuse. It is registry-scoped and refuses dirty or unpushed worktrees unless--forceor the matching granular override is supplied. It always refuses to delete the directory you are standing in and requires separate consent through--yes. Agent-owned session files are never removed.- Interrupts are handled. SIGINT and SIGTERM exit 130 and 143 after rollback
where applicable, with the stable codes
interrupted_sigintandinterrupted_sigterm. A running repository setup hook, and every process it started that remained in its process group, are terminated before rollback removes the worktree; one that left the group by callingsetsid()cannot be reached, and is reported rather than waited for (details). Cleanup cannot restore files Git already removed, so it stops the active Git process group and reports that removal may be partial. - No network, no telemetry.
agent-forkmakes no runtime network calls and collects no data (details).
By default, agent-fork carries a dirty submodule the same way it carries any
other uncommitted state: the child sees the same staged, unstaged, and
untracked content inside the submodule that the parent had, at every nesting
depth. This includes submodules the parent's own index does not record as
carriable on its own — for example a submodule advanced to a commit only its
own HEAD knows about, which a plain git worktree add cannot represent.
Per submodule, the recipe is: skip it if the parent itself never initialized
it and its checkout path is empty; otherwise refuse cold paths containing
content before mutation. An initialized copy is cloned offline from the
parent's own checkout rather than the remote. Effective url.*.insteadOf
configuration that could rewrite that pinned local path is refused, and the
clone command independently denies network protocols. The fork restores every
remote.origin.url value in its original order, checks out the parent
submodule's exact commit detached, copies its working-tree state, and recurses.
Verification re-checks initialization, commit identity, detached state, exact
remote URL values, and content, so a wrong commit or changed remote fails the
fork rather than passing silently. Unmerged index stages inside any initialized
submodule are also refused before branch or worktree creation.
The one fidelity limit: only the ordered remote.origin.url values are restored inside each
carried submodule. Other configuration — fetch refspecs, sparse-checkout
settings, hooks — is whatever the fresh git submodule add step produced, not
a copy of the parent's own local overrides. A completed fork's notices name
this limit and list what was carried.
--no-with-submodules reproduces the older, pre-carry behavior: submodules
are left as opaque gitlinks, a notice explains what was not carried, and an
unstaged submodule advance the child cannot represent is refused before any
mutation (submodule_unrepresentable, exit 5) rather than silently dropped.
The v1 JSON result schema is open and stable within major version 1 as documented in REQUIREMENTS.md. Incompatible CLI or schema changes require a major version change; compatible additions may appear in a minor release. Deprecated interfaces will be documented before removal.
Under any machine format, a failure prints a single error object on stderr:
{"error":{"code":"config_error","message":"cannot discover project config: /tmp/notrepo is not a worktree"}}Codes are stable compatibility identifiers; messages may gain detail without
changing their meaning. Error objects may add a documented details object;
cleanup guard refusals use the cleanup schema shown above.
Interruption errors use interrupted_sigint or interrupted_sigterm; their
details object contains the signal name and resulting process exit code, for
example {"exit_code":143,"signal":"SIGTERM"}.
entry_unreadable details contain one escaped entry object and a byte-wise
ordered deletion_blockers[] array. strict_skip_refused details contain the
byte-wise ordered skipped[] entries plus count; its human message names the
same paths.
| Exit | Meaning | Codes |
|---|---|---|
| 0 | Success | — |
| 1 | Runtime or verification failure | runtime_error, verify_failed, registry_busy, entry_unreadable, strict_skip_refused |
| 2 | Usage error or required prompt disabled | config_error |
| 3 | Agent, session, assertion, or target not found | agent_not_detected, agent_signal_incomplete, session_not_found, session_name_ambiguous, session_resolution_unavailable, session_validation_failed, cleanup_target_unknown, claude_parent_incomplete_analysis |
| 5 | Conflict or precondition refusal | conflict_branch_exists, conflict_branch_worktree, conflict_worktree_path, parent_mid_operation, repo_no_commits, unmerged_index, not_git_repository, git_version_unsupported, invalid_branch, invalid_worktree_base, invalid_worktree_name, cleanup_target_is_cwd, cleanup_dirty_worktree, cleanup_unpushed_commits, submodule_unrepresentable, submodule_cold_content, submodule_transport_unsafe, conflict_fork_registered, cleanup_registry_stale, cleanup_registry_ambiguous |
| 130 / 143 | Interrupted by SIGINT / SIGTERM | interrupted_sigint, interrupted_sigterm |
Exit 4 remains reserved because this local tool has no authentication failure class.
The default gate is hermetic: it excludes authenticated real-agent tests and the two process-group signal tests that require an unrestricted runner.
make check # environment and dependency preflight
flox activate
just all
just check-matrix
just strict-collect
just clean-install
just test-git-matrixjust test-git-matrix runs the intent-to-add compatibility gate with macOS
Apple Git and the GNU Git pinned by Flox. Linux runs use the system Git and the
same Flox Git.
Flox owns the reproducible Python, Git, and test toolchain. Claude Code and Codex remain host-managed prerequisites because their release cadence and platform availability differ from the four-system Flox environment, including Intel macOS. Install and update those CLIs with their native package managers.
Run the external-capability gates explicitly:
just test-live # real Claude and Codex calls; may consume account quota
just test-signals # unrestricted process-group signaling; Linux CI owns thisjust test-live first prints the selected path, resolved path, and version for
both host CLIs. It stops before pytest unless both are installed and
authenticated, ~/.claude and ~/.codex are writable, and the Anthropic and
ChatGPT endpoints are reachable. A failed real-agent command includes its
captured stdout and stderr. Override only the network probe hosts with
AGENT_FORK_LIVE_NETWORK_HOSTS=host1,host2 when an approved provider or proxy
uses different endpoints.
The design and evidence corpus is in DESIGN-DECISIONS.md, REQUIREMENTS.md, EXPERIMENTS.md, and CONFORMANCE.md.
None. agent-fork makes no runtime network calls and collects no data. Git
operations against already configured local repository metadata remain local;
package managers own installation and updates. The explicit just test-live
development gate is separate from the product runtime and calls the installed
Claude and Codex CLIs after its network/authentication preflight.
Contributions are welcome — the loop is short:
- Fork the repository and create a topic branch. A Git worktree keeps your
main checkout clean;
agent-fork forkcan create it for you. - Install dev mode and run the gate before pushing:
make check, thenflox activateandjust all(format, lint, typecheck, tests). - Write Conventional Commits
(
feat:,fix:,docs:, …). - Open a pull request; CI runs the same hermetic gate.
The design and evidence corpus — DESIGN-DECISIONS.md, REQUIREMENTS.md, EXPERIMENTS.md, and CONFORMANCE.md — explains why things work the way they do; check it before proposing behavior changes.
MIT