Skip to content

About

Fork a running coding-agent session: new branch + worktree carrying the current file state, verified, plus the exact paste command to continue (Claude Code, Codex)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

439 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

agent-fork

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.

CI Python 3.11+ License: MIT

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.

Demo

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-fork

The 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 performed

Drop --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 skill

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" becomes review-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.

Install

The skill installs with one command, for Claude Code and Codex at once:

npx skills@latest add smorinlabs/agent-fork

Claude 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

Optional: install the CLI

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-fork

Or 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 --json

After 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.

Alternative: local skill install

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-fork

Claude Code then discovers it as /agent-fork and Codex as $agent-fork.

Dev mode

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-fork

Verify the installed version and both placements:

agent-fork --version
readlink ~/.claude/skills/agent-fork ~/.agents/skills/agent-fork

The version command must print agent-fork 1.2.1. Both symlinks must resolve to this repository's .agents/skills/agent-fork directory.

Uninstall

uv tool uninstall agent-fork
npx skills@latest remove agent-fork

A symlink install is removed with rm ~/.claude/skills/agent-fork ~/.agents/skills/agent-fork; a plugin install with /plugin uninstall agent-fork.

Quickstart

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

How it works

  1. Detect the agent and parent session from CLAUDE_CODE_SESSION_ID or CODEX_THREAD_ID — or accept both explicitly as flags.
  2. Anchor to the parent's current commit and create the fork branch.
  3. Create a linked Git worktree at the destination.
  4. 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).
  5. 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.
  6. Emit the launch command for the detected agent, and record the fork in a local registry so list and cleanup can 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 reference

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.

Session inspection and validation

Use the same interface inside Claude Code or Codex:

agent-fork session
agent-fork session -o json
agent-fork session validate --agent codex --has-parent

Inspection 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.

cleanup options

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 --json

A 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.

fork options

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.

Skipped carried entries

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.

Choosing where the worktree lands

--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.

Renamed Codex sessions

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 = false

The 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

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 itself

prune 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 = true

AGENT_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.

Repository hooks

.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.

Safety and guarantees

  • Your parent worktree and session transcript are never modified. The fork is additive.
  • Verification is on by default, and it compares contents. Matching git status output 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 as apply.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-ignored may copy secret-bearing files such as .env between 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 lstat and 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.
  • cleanup is deliberately hard to misuse. It is registry-scoped and refuses dirty or unpushed worktrees unless --force or 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_sigint and interrupted_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 calling setsid() 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-fork makes no runtime network calls and collects no data (details).

Submodules

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.

Compatibility policy

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.

Exit codes and error catalog

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.

Development

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-matrix

just 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 this

just 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.

Telemetry and networking

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.

Contributing

Contributions are welcome — the loop is short:

  1. Fork the repository and create a topic branch. A Git worktree keeps your main checkout clean; agent-fork fork can create it for you.
  2. Install dev mode and run the gate before pushing: make check, then flox activate and just all (format, lint, typecheck, tests).
  3. Write Conventional Commits (feat:, fix:, docs:, …).
  4. 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.

License

MIT

About

Fork a running coding-agent session: new branch + worktree carrying the current file state, verified, plus the exact paste command to continue (Claude Code, Codex)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages