Use this page for stable command behavior, JSON fields, exit status, and guarded
automation contracts. Run kwt <command> --help for the complete flags on the
installed version.
For task-first guidance, start with the quickstart or worktree lifecycle. Agent and terminal clients should read Agent workspaces before using session endpoints. Applications choosing between the Go package, daemon, CLI, tmux, and SSH boundaries should start with Embed and connect kwt. Pull-request imports have their own protected automation contract.
Kwt requires Git 2.20 or newer. kwt pr import requires Git 2.42.0 or newer on
macOS and Linux, or Git for Windows 2.53.0.windows.3 or newer. kwt doctor and
the kwt prune --expired or --merged policies require Git 2.31 or newer:
maintenance inventory relies on git worktree list --expire, and structural
repair uses git worktree repair.
Workspace attachment requires tmux 2.1 or newer. Kwt identifies the current
client's server with tmux's #{pid} format; it does not run a separate version
preflight. A non-numeric PID response blocks attachment with tmux 2.1 guidance.
Other PID lookup failures report the underlying tmux error.
| Command | Purpose |
|---|---|
kwt, kwt tui |
Open the cross-project and multi-machine dashboard. |
kwt add |
Create a worktree and optionally launch its workspace. |
kwt branches |
List branches available for a new worktree. |
kwt open |
Open or establish a workspace session. |
kwt list |
List worktrees. |
kwt status |
Show Git status, sync state, and activity. |
kwt changes |
Inspect changed files in one exact worktree. |
kwt projects |
List registered project repositories. |
kwt pr |
Discover and import pull requests through JSON. |
kwt get |
Print a matching worktree path. |
kwt cd |
Open a shell in a matching worktree. |
kwt exec |
Run a command in a matching worktree. |
kwt remove |
Delete a worktree, optionally its branch. |
kwt doctor |
Inspect or repair structural worktree consistency. |
kwt prune |
Remove live worktrees by an explicit policy. |
kwt sync |
Publish and inspect multi-machine sync state. |
kwt ssh |
Resolve, hold, and use reviewed OpenSSH routes. |
kwt tmux |
Manage standalone tmux sessions. |
kwt workspace |
Manage directory workspaces. |
kwt config |
Read and write config values. |
kwt daemon |
Manage the background local service daemon. |
kwt serve |
Run the local service daemon in the foreground. |
kwt completion |
Generate shell completion and integration. |
kwt version |
Show version and build information. |
kwt add -b fix/parser-race
kwt add --from origin/fix/review fix/review
kwt branches --json
kwt open parser
kwt open /path/to/worktree --start-session
kwt status
kwt changes --json
kwt changes /path/to/worktree \
--expected-repository github.com/acme/widget \
--expected-generation <worktree.generation> --json
kwt pr list --project github.com/acme/widget --json
kwt pr import 17 --project github.com/acme/widget \
--expected-repository github.com/acme/widget \
--expected-registration <project.registration_fingerprint> \
--start-session --json
kwt sync status
kwt exec fix/parser-race -- go test ./internal/parser
kwt doctor
kwt prune --merged --dry-run
kwt workspace add ~/notes
kwt workspace list
kwt workspace list --json
kwt open ~/notes --start-session
kwt config get layouts.default
kwt config set --local layouts.default stack
kwt daemon start
kwt daemon status
kwt daemon status --json
kwt daemon restart
kwt daemon stop
kwt servekwt daemon start replaces a compatible running daemon only when the invoking
build is provably newer and daemon.auto_restart is "newer". kwt daemon restart refuses a provable downgrade and also refuses different builds whose
order is unknown, including distinct revisions with the same source commit
time. Use kwt daemon stop followed by kwt daemon start as the explicit
override when you intend to install an otherwise unordered build. Replacement
and restart print drain progress to stderr before waiting; JSON stdout and
ordinary command exit behavior remain unchanged.
kwt projects, kwt list, kwt changes, kwt remove, and TUI
inventory/removal auto-start or reuse a compatible local daemon. CLI inventory
requires a current refresh and fails if one cannot complete; it never prints
cached data. When available, the TUI paints the daemon's last-known-good cache;
a cold start waits for the initial current inventory. It then refreshes the
displayed repository with Git status, and refreshes the global catalog without
status once in the background. Cached rows permit shells in directories that
still exist and attachment to sessions that Kwt re-verifies as live. Mutations
and new session creation wait for current inventory. SSH route resolution and
connection leases use the same-machine daemon. For kwt ssh exec and kwt ssh copy, the daemon owns the reviewed route and lease while the foreground kwt
process runs the system SSH or SFTP child and streams its output.
Successful kwt list --json and kwt projects --json output remains a bare
top-level array. A daemon or inventory failure instead writes this shared
shape to stdout:
{
"error": {
"code": "daemon_draining",
"message": "the kwt daemon is draining",
"retryable": true,
"details": { "drain_deadline": "2026-08-10T01:02:03Z" }
}
}code, retryable, and documented detail types are the machine contract;
message is explanatory prose and may change. A corresponding single human
line remains on stderr. list daemon/inventory failures exit 1.
projects preserves its existing command contract: argument and not-found
failures exit 2, operational failures exit 1, and existing mutation codes
such as registration_changed retain the same envelope. Kwt does not
normalize unrelated commands' exit behavior. Exit 255 remains unused so an
SSH caller can distinguish a remote-shell transport failure.
Daemon ownership must not buffer human-facing operation progress. Removal
continues to print each selected worktree's result as soon as that target
finishes. SSH lifecycle uses the bounded operation.stream.v1 transport for
ordered progress and prompts. If longer mutations such as add, pull-request
import, doctor, or prune move behind the daemon, their status events must
likewise reach CLI stderr as they occur; machine-readable stdout and established
exit codes remain unchanged. Progress and warnings are flushed to stderr,
prompt responses are bound to the prompt that requested them, and only the
owning command writes its final stdout result. Advertising the capability alone
does not change a command's execution owner or user-facing behavior.
An interrupted client may resume one retained stream from its last accepted
event sequence on the same daemon. If the daemon or retained result is gone,
kwt reports operation_outcome_unknown; it does not repeat the domain command
and risk duplicating a mutation. This outcome is always non-retryable because
the mutation may already have completed.
kwt ssh resolve build.example.com --json
kwt ssh resolve 2001:db8::42 --user deploy --port 2200 --jsonkwt ssh resolve <hostname> [--user USER] [--port PORT] asks the same-machine
daemon for the effective OpenSSH route. JSON is the native-consumer contract;
human output prints only credential-free targets in connection order. Stable
kwt domain failures use the shared error envelope and exits 1 or 2, never
SSH's reserved exit 255.
Resolution preserves explicit user and port precedence, aliases, raw IPv6
command grammar, and direct ProxyJump order. POSIX hosts evaluate ssh -G
inside the account login shell with nonce-framed stdout; Windows invokes
system OpenSSH directly. Opaque ProxyCommand routes and a jump host that adds
another proxy route return ssh_route_unreviewable. The command only observes
configuration: it does not connect, approve trust, prompt for credentials, or
create a ControlMaster.
Execution policy kwt.openssh.projection.v1 emits
CanonicalizeHostname=no and resolved HostName, User, Port,
and HostKeyAlias in fixed order for each route target. Snapshot targets are
ordered in connection order. A downstream projection is not a standalone
direct-connect command: the current connection-lifecycle owner must combine it
with master-backed proxy transport through the preceding prepared target. Its
positive directive set is:
- trust and crypto:
UserKnownHostsFile,GlobalKnownHostsFile,KnownHostsCommand,RevokedHostKeys,HostKeyAlgorithms,KexAlgorithms,Ciphers,MACs,RequiredRSASize,CASignatureAlgorithms,CheckHostIP,HashKnownHosts,VerifyHostKeyDNS,VisualHostKey, andFingerprintHash; - network selection:
AddressFamily,BindAddress, andBindInterface; - authentication:
AddKeysToAgent,CertificateFile,EnableSSHKeysign,ForwardAgent,GSSAPIAuthentication,GSSAPIDelegateCredentials,HostbasedAcceptedAlgorithms,HostbasedAuthentication,IdentitiesOnly,IdentityAgent, explicitly configuredIdentityFilevalues,KbdInteractiveAuthentication,PasswordAuthentication,PKCS11Provider,PreferredAuthentications,PubkeyAcceptedAlgorithms,PubkeyAuthentication,SecurityKeyProvider, andUseKeychain; - client environment and behavior:
EscapeChar,SendEnv, andSetEnv.
Path-bearing authentication values and SetEnv are represented only as
owner-private ephemeral configuration lines, not argv or diagnostics. Every
unlisted directive—including forwards, commands, and user ControlMaster
settings—still changes route identity but is never replayed for execution.
kwt ssh lease build.example.com \
--route-identity <identity-from-ssh-resolve> \
--projection-policy kwt.openssh.projection.v1 \
--host-key-policy review \
--jsonkwt ssh lease is the long-lived native-client bridge to the same-machine
daemon. Native clients that already reviewed a snapshot pass
--route-identity and --projection-policy; kwt refuses a changed route.
When those flags are omitted, the same CLI invocation resolves the route and
immediately acquires it, avoiding a second helper process while retaining the
daemon's pre- and post-connection revalidation. The daemon prepares
every ProxyJump hop in order, and streams operation events as NDJSON. Prompt
capable clients use --host-key-policy review, which requires an explicit
OpenSSH review prompt unless the route already requires a known key.
Unattended clients use --host-key-policy strict, which accepts only keys
already trusted by OpenSSH and never modifies an empty known-hosts file.
Events may occur repeatedly; the client answers each with one
{"prompt_id":"...","value":"..."} line on stdin. The completion event
contains generation-bound OpenSSH arguments. Each prompt carries the daemon's
deadline, so blocked input ends when the prompt expires and the command reports
the daemon's terminal timeout. OpenSSH confirmation requests use the
ssh_host_key kind and are not sensitive; credential and keyboard-interactive
requests use ssh_authentication and are sensitive. Every prompt includes its
credential-free logical target, effective target, display target, zero-based
hop index, and route hop count in details, so a native client can identify
which direct or ProxyJump target controls the prompt. Host-key prompts also
include a host_key object containing the reviewed host, algorithm, and
fingerprint, so native clients do not parse OpenSSH prose. The prompt message
remains OpenSSH's original text. Other OpenSSH confirmation shapes are
preserved as sensitive ssh_authentication prompts without a claimed host-key
identity. Unhinted prompts are always handled this way, even when their prose
resembles a host-key question. The process keeps the lease alive
while stdin remains open, touches it every ten seconds, and releases it when
stdin reaches EOF or the command is canceled. Progress and warnings are written
as they occur rather than buffered.
Multiplexed-client arguments reapply the final target's reviewed
ForwardAgent, SendEnv, SetEnv, and EscapeChar settings. Private
SetEnv values remain in daemon-owned ephemeral configuration rather than
appearing in argv.
The daemon lease bridge requires a persistent OpenSSH master. A platform that
cannot provide multiplexing returns ssh_route_unreviewable instead of
publishing direct-connect arguments outside the daemon's prompt and trust
boundary. The embeddable Go API may still return a masterless lease to an
in-process owner that provides that direct execution boundary itself.
The daemon expires a lease after thirty seconds without a successful touch.
Release and expiry cleanup are bounded; a timed-out cleanup retains the lease
for a later retry instead of releasing its daemon reservation.
After final release, ssh.idle_timeout controls how long a non-agent-forwarding
master remains warm. Agent-forwarding masters disconnect immediately. Daemon
replacement reports its active lease count, waits through
daemon.replacement_grace, then invalidates remaining leases and terminates
their verified masters; a successor never adopts them as live connections.
kwt ssh exec build.example.com -- uname -a
kwt ssh copy build.example.com ./kwt /tmp/kwt.incomingkwt ssh exec and kwt ssh copy resolve and acquire a daemon-owned route,
run one system OpenSSH client through that generation-bound master, hold the
lease through an authenticated owner stream while it runs, and release the
lease afterward. The owner stream remains live when job control suspends the
foreground CLI, but closes automatically if that process exits. Kwt—not its
caller—constructs the SSH or SFTP invocation,
including ProxyJump transport, port grammar, private projection files, and
fail-closed control-socket behavior. Remote stdout and stderr are streamed as
they arrive; connection progress is streamed to stderr unless --quiet is
set. An OpenSSH or SFTP exit status is preserved, including SSH's status 255,
while failures before client execution keep kwt's stable typed error surface.
Destructive callers can pass kwt ssh exec --route-identity <reviewed> to
require the same route they previously reviewed; route drift fails as
ssh_configuration_changed before the remote command starts.
Machine callers add --json: a kwt failure before SSH or SFTP starts writes the
shared error envelope to stdout, while successful client execution continues
to stream the remote command's unmodified output. Cleanup failures after the
client starts are reported only on stderr and never append framing to stdout.
Both commands default to --host-key-policy strict for unattended callers.
Use --host-key-policy review from a terminal to permit the daemon's bounded
host-key and authentication prompts. A nonterminal caller never answers a
prompt implicitly and receives ssh_interaction_required. Copy accepts one
local file and one remote path, resolves the local source to an absolute
literal path, escapes SFTP batch metacharacters in both paths, and never embeds
the destination in a remote shell command. It translates the reviewed SSH
projection into SFTP's option grammar internally, so consumers never handle
control-path quoting or the SSH/SFTP -p versus -P distinction.
When kwt add -b creates a branch, it fetches origin and starts from its
default branch. If that remote base is unavailable, it falls back to local
main, then master, then the branch checked out in the primary worktree.
kwt add <branch> checks out an existing local branch. Use
kwt add --from <remote-ref> <branch> when a remote candidate must become a
local tracking branch. Shorthand such as origin/topic is accepted, but kwt
verifies it against fetched refs and passes the full refs/remotes/... identity
to Git. Neither existing-branch path copies files, runs setup commands, or
launches a workspace; --layout and --select-layout are rejected.
Git branch mutation and checkout run with an empty hooks directory, configured
smudge and process filters disabled, and kwt credential variables removed.
Environment references in the source-derived branch name remain literal when
kwt builds the destination path. Submodules are not recursively updated during
creation; inspect the superproject first, then update submodules explicitly
after acknowledgement.
The created worktree participates in ordinary status and fleet observation.
Review the checkout and run kwt open <worktree> as the explicit
acknowledgement that opts in to its layout and pane commands.
kwt branches --json emits only candidates not already checked out, with
name, a source-qualified display label, the full source ref, and
is_remote fields for interactive clients.
With no argument, kwt open fuzzy-picks a worktree. A pattern narrows the
cross-project list and opens the sole match directly. An exact registered
directory workspace path resolves before Git worktree discovery. Kwt creates
or repairs the canonical tmux workspace on the dedicated kwt server with its
resolved layout before attaching. On POSIX systems, the equivalent manual
inventory command is:
env -u TMUX_TMPDIR tmux -L kwt list-sessionsKwt deliberately removes TMUX_TMPDIR for the canonical server, so clients
that attach independently, including remote clients, must do the same.
An exact worktree-root path is resolved directly from Git before pattern matching, including registered primary checkouts and linked worktrees outside the configured global worktree base.
On Unix-like systems, an external direct or protected attachment replaces
the kwt process with the tmux client. tmux therefore owns signal handling and
the final exit status, and no waiting kwt parent remains. Windows retains a
waiting parent because it has no Unix process-replacement primitive.
A direct open from a client already on the resolved server switches that
client. From a different tmux server, kwt removes TMUX and TMUX_PANE and
starts a nested client on the resolved endpoint. Detaching returns to the outer
server; because both clients normally use the same prefix, reaching the inner
client may require sending the prefix twice. Protected attachment uses the same
cross-server nesting mechanism while retaining its stricter environment rules.
kwt open <exact-workspace-path> --start-session performs the same layout and
session bootstrap without attaching a client. Use it before an external
direct tmux client attaches to a session that may not exist yet. Registered
directory paths resolve from the workspace registry; worktree paths resolve
directly from Git rather than the global worktree base. This keeps automation
noninteractive and supports both plain directories and linked worktrees stored
outside that base.
Protected pull-request imports remain restricted to kwt pr attach.
During rollout, a verified matching session on the default server is adopted
instead of creating a parallel session. If matching sessions
exist on both servers, the kwt server wins. Mixed old and new Kwt binaries can
still create parallel same-name sessions because older binaries neither use nor
inspect the dedicated server. Once a new binary has created canonical sessions,
do not run destructive workspace operations such as remove, prune, or repair
with an older Kwt binary: its removal guard cannot see those sessions. A
coordinated upgrade of all cooperating clients is required before enabling the
new topology on a shared host.
workspace add [path] registers a plain directory. The workspace remove
command accepts its name and unregisters it without deleting the directory or
killing a live tmux session. workspace list reports registered directories
and session state.
workspace list --json emits an array of objects with name, canonical
absolute path, effective session_name, and boolean session_live. An empty
registry emits [] with no table prose. When a workspace was renamed while its
session remained live, session_name reports that matching live session so
clients can attach to it; otherwise it reports the canonical name kwt will use
when establishing the session.
The command obtains inventory from the same-machine daemon, including when a remote shell invokes kwt. Its human output and machine-readable schema are unchanged; freshness metadata stays in the daemon API envelope and is not added to the top-level JSON array.
--json emits an array of objects with path, branch, commit_hash,
is_main, created_at (worktree directory mtime), generation (the durable
identity for conditional removal), repository (the host/owner/name slug,
or a local/<path> fallback for a repository without a usable remote — see
below), session_name, tmux_socket_name, and tmux_attach_mode.
When a branch is switched or renamed while its session remains live,
session_name reports that verified live name even though its branch component
is stale. Otherwise it reports the canonical name kwt will use for the next
launch. Kwt verifies the worktree generation before associating a live session,
so a worktree recreated at the same path does not inherit the previous session.
tmux_socket_name selects the endpoint; tmux_attach_mode selects direct or
protected attachment behavior. For a stopped workspace, inventory
reports the intended kwt/direct endpoint. It reports the empty default
socket with direct only while a matching adopted session is live. Protected
pull-request worktrees report their workspace-specific socket with
protected once provenance verifies that endpoint. An unresolved protected
entry omits tmux_socket_name; clients must use tmux_attach_mode to
distinguish that unattachable state from an adopted default-server session.
Inventory is a snapshot. kwt open <exact-path> --start-session --json is the
authority for the immediately following direct attachment because it reports
the endpoint where the session was actually established. Run it before an
attach-only client, or use kwt pr attach <path> when
tmux_attach_mode is protected. This lets kwt create the session when needed
without a client creating it bare or bypassing protected attach policy. See
Attaching from other tools before using
new-session.
kwt open and dashboard open actions refuse protected pull-request imports
and direct the user through kwt pr attach.
created_at and generation are populated in both local and -g mode.
kwt changes
kwt changes /path/to/worktree
kwt changes /path/to/worktree --json
kwt changes /path/to/worktree \
--expected-repository github.com/acme/widget \
--expected-generation 0123456789abcdef0123456789abcdef \
--jsonchanges returns one point-in-time local change set for one exact registered
worktree. The optional path defaults to the current directory. The foreground
CLI converts the literal path to an absolute path without resolving it to a
different spelling; inventory then applies the platform's native canonical
path comparison and returns the authoritative worktree path.
Human output identifies the quoted repository and worktree path, durable
generation, and observation time before listing quoted changed paths. Each
path has separate staged and working tree states; - means that side has no
change, including an unmerged index slot that is not a staged change. Rename
output shows the quoted original and resulting paths. A clean worktree says
No changed files.
--json emits one InspectionResult object:
{
"worktree": {
"repository": "github.com/acme/widget",
"path": "/work/widget",
"generation": "0123456789abcdef0123456789abcdef"
},
"changes": {
"state": "modified",
"summary": {
"modified": 1,
"added": 0,
"deleted": 0,
"untracked": 0,
"staged": 0,
"conflicts": 0
},
"files": [
{
"path": "README.md",
"worktree": "modified"
}
]
},
"observed_at": "2026-08-20T18:00:00Z"
}files is always present, including "files": [] for a clean worktree.
Paths are JSON strings, so valid UTF-8 names containing spaces, tabs, newlines,
and native Windows path forms do not require a secondary delimiter convention.
Ill-formed UTF-8 Unix filename bytes are not a lossless part of this JSON
contract. File order is deterministic. index and worktree describe the two
porcelain-v2 sides; original_path is present for a rename or copy.
summary.staged is orthogonal to the mutually exclusive semantic buckets and
does not include conflicted paths.
An untracked-only worktree has overall state: "modified"; use
summary.untracked and each file's worktree: "untracked" value to distinguish
it from tracked modifications. If the exact change list exceeds the bounded
status-output limit, the command returns non-retryable inspection_failed with
worktree change list is too large to inspect rather than returning partial
file records.
The optional expected repository and generation flags are independent
compare-and-fail guards. A mismatch returns retryable registration_changed;
the caller must refresh instead of presenting the discarded snapshot. The
other stable command codes are invalid_request, not_found, and
inspection_failed, plus the shared daemon and inventory codes that can occur
while obtaining current authoritative inventory. invalid_request and
not_found exit 2; other domain failures exit 1. With --json, failures
use the shared { "error": ... } envelope. Error code, retryable, and
documented detail types are contractual; message remains explanatory prose.
Exhausting the command's internal Git budget returns retryable
inspection_failed; cancellation by the caller remains a context cancellation.
Inventory comes from the same-machine daemon, but both durable-generation
checks and the bounded porcelain-v2 status read execute in the invoking
foreground process. The command strips Kwt-protected credentials, disables
optional Git index writes, uses a locale-independent parser, and honors the
five-second Git-operation budget; cleanup is additionally bounded to 100
milliseconds when a descendant retains an output descriptor. kwt changes
requires the daemon's config-bearing inventory capability and fails closed if
effective configuration is absent. A compatible older daemon without that
capability returns daemon_incompatible; with daemon.auto_restart = "newer"
a provably newer client may replace it, while "never" requires an explicit
daemon restart. The command does not fetch, calculate activity or
ahead/behind state, run diff or numstat, generate patches, discover siblings,
watch, or require tmux. The same command and JSON contract are available on
macOS, Linux, and Windows.
kwt tmux list temporarily unions Kwt-managed standalone and workspace
sessions from the dedicated kwt server and the default server during the
adoption window. Human,
picker, attach, and kill surfaces display [kwt] or [default] so equal-name
sessions remain distinguishable; JSON includes tmux_socket_name and
tmux_attach_mode. New standalone sessions are created only on the dedicated
server. Attach and kill retain the selected endpoint rather than inferring it
from the session name.
kwt tmux run --context rejects wt and workspace, which are reserved for
managed worktree and directory-workspace session names.
--if-generation <id> makes a single-worktree removal conditional on the
generation value returned by kwt list --json. Kwt stores this random
identity in the worktree's Git administrative directory and compares it while
holding the repository's worktree-mutation lock, so automation cannot delete a
replacement checkout created at the same path. Ordinary directory changes do
not alter the generation.
Removal is executed by the same-machine daemon through kwt's public Go service.
The daemon reopens the named repository and revalidates the exact path,
generation, main-worktree status, and lock state while holding the repository's
cross-process mutation lock. It also performs generation-safe registry cleanup.
Immediately before removal, it rejects a worktree used as the current working
directory of a visible live process, including a process whose working
directory is nested inside the worktree. The conflict lists the process IDs to
stop. If the operating system refuses working-directory inspection for a live
process owned by the current user, removal also fails closed with that PID.
--force bypasses both process checks and Git's dirty-worktree refusal.
The CLI and TUI retain selection, output, fleet publication, and tmux-session
cleanup; stale inventory alone never authorizes deletion.
Automation that has confirmed terminal-session state can add
--if-session-name with --if-session-absent.
--if-session-socket-directory selects an explicit TMUX_TMPDIR.
--if-session-socket-name selects an exact named socket such as the
workspace-specific server used by an imported pull request. Supply both
selectors for a legacy named socket located under an explicit TMUX_TMPDIR;
otherwise omit the directory to use the canonical named-socket location.
These flags require --if-generation. Kwt revalidates absence while holding
the same project lifecycle fences used by guarded kwt open. A live-session
identity is rejected with a retryable conflict: callers must explicitly stop
the exact session, freshly confirm absence, and retry. Kwt does not claim an
atomic live-session freeze on a shared tmux server because tmux command queues
can continue changing topology while an external signal helper starts.
Known Git removal failures use the stable removal_failed code and preserve
their credential-sanitized message and partial-result fields across the daemon
boundary. Unexpected failures use internal and withhold their cause.
If a removal response is lost, the client requests a fresh bounded repository
inventory. A missing original generation is reported as an irreversible partial
result so fleet publication, TUI refresh, and session cleanup still run. If the
postcondition cannot be observed, the client reports an indeterminate transport
failure and requests fleet/UI refresh without claiming removal or killing tmux.
Requires Git 2.31 or newer.
kwt doctor # inspect without changing anything
kwt doctor --fix # apply only confirmed, unambiguous repairs
kwt doctor --json # emit exact findings for automation
kwt doctor --quiet # print nothing; use the exit statusDoctor reads project registrations, Git worktree metadata, filesystem
backlinks, the global worktree directory, and live registry paths. The human
report starts with a summary, lists Ready to fix findings before Needs
review, and omits healthy repositories. Long home and temporary paths are
shortened for display; --json keeps exact paths and versioned evidence.
After --fix, the report first lists each completed repair under Fixed,
followed by findings that remain after the rescan. JSON exposes the same
repairs in fixed and counts them in summary.fixed_findings. Use --quiet
when only the exit status matters; it cannot be combined with --json.
Doctor reports its current phase on stderr while it loads inventory, inspects
worktrees, applies fixes, and verifies repairs. A terminal uses one updating
line; redirected stderr receives bounded milestones. --json still writes one
report document to stdout. --quiet suppresses both the report and progress.
If a registered project path is confirmed absent, doctor searches only its bounded local inventory for the same credential-free repository identity:
- one matching main repository produces
project_path_movedand--fixupdates the registration to that verified absolute path; - one matching main repository that is already registered produces
stale_project_registrationand--fixremoves only the unchanged duplicate; - multiple missing registrations that claim the same matching repository
produce
ambiguous_project_relocationand require you to choose which registration should remain; - no match produces
stale_project_registrationand--fixremoves the unchanged registration; - multiple matching clones produce
ambiguous_project_relocationand require a manual choice; and - path-derived, malformed, or otherwise unsafe identities remain
project_unreachableand are never changed automatically.
Project changes use full-entry compare-and-swap, preserve the name and last
touched time, and become safe no-ops if the path, target, or global config
changes concurrently. A relocation is also a no-op if another project becomes
registered at the target before the config transaction commits. Doctor reloads
both global config and the registry before its final report, so a successful
fix can exit healthy while a no-op remains an
exit-1 finding. Generation-less registry adoption also rechecks the live Git
generation while holding the repository mutation lock, so a replacement at the
same path is never assigned the inspected worktree's generation.
kwt doctor --fix repairs only uniquely owned backlinks and confirmed-absent
Git or registry records. It can adopt a generation-less registry record with
compare-and-swap after clearing inherited expiration metadata, including when
the registry path is a symlink alias for the live Git worktree. A nonempty
registry/Git generation mismatch is a possible replacement conflict and stays manual.
Because Git can repair multiple backlinks in one operation, doctor
skips backlink repair for a repository unless every repairable backlink is in
the validated fix scope. Doctor repairs backlinks before native metadata
pruning, prunes only when every removable record is in the validated fix scope,
revalidates under each repository's mutation lock, and rescans before reporting
the final state. A linked worktree's administrative directory is accepted only
when a full scan of the repository's worktree metadata finds exactly one
reverse backlink to that path. Registry entries are not reported or changed
while their creator still holds the path lock. Tokens abandoned after a creator
exits are inspected and cleaned only after doctor acquires that same lock.
Doctor groups registry paths by canonical filesystem identity. Equivalent
aliases for one live worktree appear under Duplicate registry paths, and --fix
atomically collapses the unchanged group to the path spelling recorded by Git.
Equivalent aliases for a worktree path confirmed absent are removed as one
unchanged group during the same fix run after absence is rechecked. Aliases with
different generations, expiration, source-review state, or other policy
metadata, incompletely inspectable groups, and aliases for an existing path
that is not a verified Git worktree remain under Needs review. It
normalizes legacy registry and configured project URLs before comparison and
output. Values that cannot be converted to credential-free identities are
redacted or omitted; reachable projects fall back to the canonical live origin
or local identity. Origin-based expiration records are valid when they match
the verified origin for that worktree, even when the configured project names
an upstream repository. It never removes a live worktree. A live worktree
without a valid Git generation is manual until a normal listing from the
verified repository (kwt list) adopts it; rerun kwt doctor --fix afterward
to reconcile a generation-less legacy registry record. Project registration
repair runs afterward, so Git, registry, and global-config locks are never
nested.
Exit status is 0 when healthy, 1 when findings remain, and 2 when
inspection or requested fixes cannot complete.
Prune requires Git 2.31 or newer and exactly one live-removal policy:
kwt prune --expired --dry-run
kwt prune --expired
kwt prune --expired --force
kwt prune --merged --dry-run
kwt prune --merged--expired evaluates registered expiration times. An expired entry whose path
is already absent reports doctor_required and points to kwt doctor --fix;
expiration pruning no longer silently unregisters it. --force is available
only for expiration policy and never bypasses the generation requirement.
Ignored build artifacts do not count as dirty for expiration policy and are
removed with the worktree; use --dry-run to preview the decision. Kwt
rechecks the complete expiration record before removal. If another command
extends or clears the expiration after inspection, the worktree is preserved
with expiration_policy_changed.
--merged inventories linked worktrees from configured projects, the global
worktree directory, and finalized registry entries, then confirms GitHub
pull-request evidence before any removal begins. Registry entries still owned
by a creation token remain doctor-only. Imported worktrees must match their
complete provenance, including canonical live workspace and main-repository
paths; an existing
symlink alias is equivalent to its resolved path. New imported-worktree
provenance also carries the durable generation, preventing an old record from
matching a replacement at the same path. A provenance-backed candidate also
requires its current alias-connected project registration. Kwt holds that
project authority while it probes the exact protected tmux endpoint, removes
the worktree, and compare-and-swap deletes provenance. A live endpoint reports
protected_session_live; an unavailable or indeterminate endpoint reports
protected_endpoint_inventory_incomplete. Both preserve the worktree and
provenance. Protected sessions created by released kwt versions under an
explicit TMUX_TMPDIR remain attachable and continue to block pruning when the
invoking client supplies that same directory; new sessions use kwt's canonical
owner-specific socket location. Ordinary worktrees must have
an exact configured upstream repository and branch read from that linked worktree,
and their current HEAD must exactly match one associated PR head SHA
with a non-null merged timestamp. GitHub repository redirects are resolved
before PR lookup and checked against imported provenance aliases. Redirects for
the upstream source repository are also resolved for PR matching and diagnostic
head lookup, while the observed upstream remains the lock-time removal
condition. This recognizes squash and rebase merges without using local
ancestry. A configured project remains the PR base when its local origin is a
fork; kwt reads the observed origin from each linked worktree separately and
revalidates it, the local branch, and the upstream under the repository lock.
If a configured project's repository identity is missing or invalid, the
candidate reports doctor_required; only unconfigured discovery roots may use
their live origin as the PR base. That fallback also requires every configured
project root to be inspectable. If one is stale or temporarily unavailable,
globally and registry-discovered candidates report doctor_required before
GitHub lookup, while candidates from healthy configured projects remain
eligible. Run kwt doctor --fix to repair or remove the stale registration and
restore unconfigured discovery.
When global discovery finds a repository that is not configured, each
worktree's own observed origin becomes its PR base; one worktree's origin is
never reused for a sibling. An expiration registry record may identify either
that verified origin or the configured project. A missing or noncanonical
origin preserves the worktree before provider lookup. Advanced heads, ambiguous matches,
closed-but-unmerged PRs, and missing generations are preserved with explicit
reasons. Removals execute from the resolved main repository,
so pruning one globally discovered worktree does not strand later candidates.
Multiple registry paths that resolve to the same worktree are ambiguous and
report doctor_required instead of allowing path-order-dependent removal.
Git commands run against candidate worktrees without kwt's GitHub or fleet
credentials in their environment, including during lock-scoped validation and
removal. This prevents worktree-selected filters, hooks, or filesystem monitors
from receiving those tokens.
Kwt proves that the associated pull request merged before it checks local files. An interactive run asks before removing a confirmed merged worktree that contains tracked, untracked, or ignored files. Approval removes the complete worktree directory and all files added before removal. The prompt defaults to no. A branch that advanced after the pull-request head remains a hard stop.
--dry-run never prompts and reports would_require_confirmation. JSON and
redirected-input runs never prompt or remove a dirty candidate; they report
confirmation_required and exit 1. A declined interactive prompt reports
confirmation_declined, continues to later candidates, and does not make an
otherwise successful run fail. There is no fleet-wide --force mode for
merged pruning; approval applies only to the candidate named by the prompt.
Prune JSON uses schema_version: 2 for these outcomes. Human output groups
repeated reasons and remediations so large candidate sets remain readable.
An individual path under the global worktree directory that is no longer a
usable Git worktree reports doctor_required; it does not prevent other
repositories from being evaluated. An incomplete directory traversal still
stops inspection with exit status 2, because kwt cannot know what was missed.
If Git deregisters a worktree but cannot remove every file, prune completes its
guarded registry and provenance cleanup, publishes the new fleet state, and
reports cleanup_incomplete with the residual path for manual inspection.
Both policies support --dry-run and --json. Exit status is 0 when all
confirmed candidates were handled or no candidate exists, 1 for safety skips,
provider failures, or incomplete cleanup, and 2 when inspection or command
usage cannot complete. Bare kwt prune is an exit-2 usage error: run
kwt doctor --fix for the stale-metadata behavior that the bare command used to
perform. JSON result reports use stdout without duplicating human result
summaries on stderr. Progress always uses stderr: terminals show one updating
line, while redirected output receives bounded phase and count milestones.
Dry-runs revalidate Git's current inventory under the same
repository lock as removal and report locked_worktree or main_worktree
instead of claiming those paths would be removed.
The list action obtains a current inventory snapshot from the same-machine
daemon. projects add remains a direct foreground mutation. projects remove
is an authenticated daemon-owned transaction so it can serialize registry
changes with worktree creation, pull-request import, and protected-session
establishment.
--json emits an array of the registered project repositories ({repository, name, path, last_touched, registration_fingerprint}), so external automation
can discover main-repo paths that live outside the configured worktree base
directory without parsing the config file. repository uses the same
host/owner/name slug as kwt list --json's repository field, so the two
surfaces can be joined. registration_fingerprint is an opaque observation
token for the complete persisted entry; callers must not parse or synthesize
it.
Entries whose configured paths are missing, inaccessible, or no longer Git
repositories remain visible. Their path is the exact persisted value and
their repository is a stable credential-free identity, allowing automation
to authorize metadata removal without inspecting the checkout. Pathless
entries are omitted. Kwt does not scan for moved checkouts; running kwt in the
checkout's new location, or using kwt projects add <new-path>, updates the
existing record by repository identity.
kwt projects add <path> registers an existing Git checkout without opening
the dashboard. A linked-worktree path resolves to its main repository before
registration. For a bare-container layout, callers may pass the container
directory itself; Kwt registers its checked-out main/ worktree and excludes
the .bare/ control directory from worktree inventory. Repeating the command
updates the existing entry rather than adding a duplicate. Registration changes
serialize with protected project operations across both the old and new
repository identities.
Machine callers use kwt projects remove <exact-registered-path> --expected-repository <identity> --expected-registration <fingerprint> --json
to unregister exactly one observed project. Supply all three values from the
same current kwt projects --json entry; paths are matched byte-for-byte,
including trailing whitespace. A human invocation without --json may omit
both expected flags, in which case kwt performs one current exact-path lookup
before submitting the removal. Supplying only one flag is invalid. Kwt never
automatically refreshes and retries a registration_changed response.
Canonical stored identities are authoritative; legacy registrations use an
authoritative live Git identity when available and an exact-path local identity
otherwise. The checkout may no longer exist. The daemon validates the opaque
fingerprint before resolving identity, then verifies the project's durable
protected endpoints, including connected repository-transfer aliases, under
the shared project fence before performing a final raw registry
compare-and-swap. Any persisted-field change, including last_touched,
invalidates the fingerprint. A live protected tmux session or incomplete
endpoint authority fails closed. KWT/default-server tmux sessions do not
block removal and are never killed.
Unregistration is metadata-only: it never deletes repositories or worktrees and never kills a tmux session. Concurrent registration changes are preserved and reported as retryable. Worktree creation and pull-request import keep their existing foreground status output; the daemon fence does not make these CLI operations asynchronous or hide progress.
With --json, projects add returns status: "registered"; removal uses the
same project object with status: "unregistered":
{
"status": "registered",
"project": {
"repository": "github.com/kenn-io/kwt",
"name": "kwt",
"path": "/code/kwt",
"last_touched": "2026-07-27T11:16:16Z"
}
}Failures return a stable error envelope on stdout. invalid_repository exits
2; registration_failed exits 1. Project removal additionally reports
project_not_found with exit 2 and unregistration_failed,
protected_session_live, or protected_endpoint_inventory_incomplete with
exit 1. The retryable registration_changed also exits 1. Protected-session
errors expose only the sanitized session and protected-socket identity needed
to resolve the block:
{
"error": {
"code": "invalid_repository",
"message": "/missing is not an accessible Git repository",
"retryable": false
}
}kwt pr list and kwt pr import are the noninteractive, structured boundary
for pull-request clients. kwt owns provider calls, ref handling, branch and
workspace naming, normal worktree creation and setup, push configuration,
provenance, and tmux session naming. See Pull-request
automation for the JSON and exit-status contract.
Every imported workspace record includes tmux_attach_mode: "protected".
An attachable record also includes tmux_socket_name; an empty socket means the
protected endpoint is unresolved, not that the default tmux server should be
used.
pr import --start-session additionally establishes a blank shell-only
session without attaching, for clients that provide their own direct tmux
presentation. It does not execute configured layouts or agent commands.
Automation clients may pass --expected-repository and
--expected-registration together to bind an import to the selected
kwt projects --json record. Kwt revalidates that authority under the project
lifecycle fence before creating the worktree; a mismatch fails with retryable
registration_changed and does not import anything.
Attach with kwt pr attach <workspace.path>, which verifies the persisted
identity, creates or repairs that protected blank session when needed, and
uses attach-session -E. This is an interactive exception to the PR JSON
contract: failures before attachment remain structured, but after a successful
Unix process replacement tmux owns terminal output and the final exit status.
Automation clients may additionally pass --expected-repository,
--expected-registration, --expected-generation, --expected-session, and
--expected-socket together. Kwt revalidates that exact inventory authority
under the project lifecycle fence before creating or repairing the protected
session; a mismatch fails with retryable registration_changed and does not
touch tmux. Omit all five flags for ordinary interactive attachment.
Provenance with a durable generation applies only to that exact worktree
incarnation. If the path is later reused, stale provenance neither protects the
replacement from ordinary kwt open nor authorizes kwt pr attach or merged
pruning. Generation-less legacy provenance continues to match by its verified
path, branch, and repository identity.
A repository's repository slug is derived from its origin remote
(host/owner/name). A repository with no usable remote instead gets a
deterministic path-based fallback of the form local/<absolute-path> (path
separators normalized to /). Every surface that reports repository identity —
kwt list --json, kwt list -g --json discovery, and kwt projects --json —
resolves it through the same code, so the fallback is identical across all
three and the surfaces remain joinable even for local-only repositories.
Every workspace session kwt creates (add, open, and the TUI) applies the
same bootstrap so panes are indistinguishable regardless of which client
attaches. kwt sets default-command to the empty string, which tells tmux to
start its configured default-shell natively instead of passing a command
string through $SHELL -c. This supports valid non-POSIX shells such as fish
and tcsh and avoids running startup hooks in an extra non-login shell. For the
first pane, kwt queries the session's resolved default-shell and executes it
directly with -l after applying the session environment bootstrap. This
login-shell behavior is the parity mechanism across launchers; panes otherwise
see tmux's own TERM_PROGRAM/TERM_PROGRAM_VERSION, which tmux sets in every
pane regardless of what kwt does.
Session creation uses an inert first-pane placeholder to make that environment
ordering safe. new-session starts sleep 2147483647 as separate argv words,
so no user shell or profile runs before the session exists. kwt then installs
the session remove-markers, resolves default-shell, and replaces the
placeholder with <resolved-shell> -l. Only after that respawn does it create
the remaining panes. Thus every real shell starts after launcher state has been
masked, and the first pane's startup files run exactly once.
kwt treats a canonical set of variables as launcher state — scoped to the terminal, shell, or tool that launched kwt, not to the workspace session tmux hosts — and applies it in two places that share a single definition, with one explicit, documented exception, so they cannot silently drift apart:
- Exec-time sanitization. Every tmux invocation kwt makes execs tmux with
these variables removed from its own environment, EXCEPT
EDITORandVISUAL. If no tmux server is running yet, the invocation that starts one is what seeds that server's GLOBAL environment table; sanitizing at exec time keeps launcher state out of that table in the first place, rather than only masking it later per session. (PWD/OLDPWD/SHLVL/_are included even though they aren't terminal-integration variables: worktree directories are passed to tmux via-c, and shells re-derive these on their own.)EDITOR/VISUALare kept here because tmux itself reads them at server start to choose its default key mode (status-keys/mode-keys: vi vs. emacs), and a user'stmux.confmay consult them too; stripping them from the server's own exec environment would silently flip that behavior for every kwt-started server. - Session remove-markers. The same variables — including
EDITORandVISUAL, with no exception — are also removed from each session with a session-scoped remove-marker (set-environment -r), which masks the global/server value for that session only without touching other sessions or the server-wide environment. This covers sessions created against an already-running server whose global table predates kwt's exec-time sanitization (e.g. a server another tool started), and it is what keepsEDITOR/VISUALout of every pane's shell even though the server process itself now keeps them.
The full list: exact names __CFBundleIdentifier, EDITOR,
KWT_FLEET_TOKEN, KWT_GITHUB_TOKEN, OLDPWD, PROMPT, PROMPT_COMMAND,
PWD, RPROMPT, SHLVL, TERM_PROGRAM, TERM_PROGRAM_VERSION, VISUAL,
WINDOWID, _; and prefixes ALACRITTY_, CONDA_, FZF_, ITERM,
KITTY_, NVM_, PYENV_, STARSHIP_, VIRTUAL_ENV, WEZTERM_, WT_,
VSCODE_. Every kwt workspace also removes the exact variable named by
fleet.token_env, case-insensitively, both from tmux subprocesses and from
the session environment. Operational kwt variables such as KWT_HOME are
preserved. EDITOR and VISUAL are excluded from exec-time sanitization
only, per above; every other name in this list is treated identically by both
mechanisms.
TERMINFO is deliberately excluded from the whole list: it is functional
terminal configuration (a custom terminfo database path), not transient
launcher-integration state, and is needed for tmux attach rendering and for
pane applications resolving tmux's own TERM.
kwt applies this bootstrap when it creates a session. A session that some
other tool created — for example with tmux new-session -A -s <session_name>,
which attaches if the session exists but otherwise creates it bare — starts
without the default-command and remove-markers, so its windows would not
match kwt's until repaired.
Two rules keep external tools consistent with kwt:
- When the session already exists, attach only: for the canonical direct
endpoint on POSIX, unset
TMUX_TMPDIR, then runtmux -L kwt attach-session -t <session_name>. Use the empty/default endpoint only when the immediately preceding Kwt JSON result returnedtmux_attach_mode: "direct"with an emptytmux_socket_name. A protected result with an empty socket is unresolved and must not be attached. From a client on the same server,switch-client -t <session_name>is sufficient. Attach-only commands never create a bare session, so there is nothing to repair. - If your tool creates the session itself, apply the equivalent bootstrap:
set
default-commandto""and add a session-scoped remove-marker (set-environment -r <name>) for each launcher variable listed above (includingEDITOR/VISUAL— the exec-time exception above applies only to how the tmux server process itself was started, not to the session remove-markers). To keep the first pane clean as well, create it with an inert direct-argv placeholder, install the markers, resolve the session'sdefault-shell, and respawn that pane with the resolved shell and-l.
kwt is also self-healing here: the next time it attaches to a session it finds
already running, it re-applies the safe bootstrap subset (default-command
plus the remove-markers — never construction or pane commands), so a session
another tool created bare converges on consistent behavior for windows opened
after that attach.
The canonical endpoint ignores ambient TMUX_TMPDIR; this is why the manual
commands above explicitly unset it. A remote client must follow the same rule
as the remote Kwt process or it will address a different socket path.
An external client must also sanitize every attach and reconnect environment;
unsetting only TMUX_TMPDIR is insufficient because tmux can copy client
variables named by update-environment into session state. Apply the same
policy as Kwt's AttachSanitizedEnviron: remove every launcher variable listed
above, Kwt's GitHub and fleet token variables, and the dynamically configured
fleet.token_env name before executing tmux. This requirement applies to
shared direct endpoints as well as protected ones. A client that cannot
reproduce the current policy should delegate attachment to Kwt instead of
launching tmux directly.
PR imports use a stricter reuse boundary. Every import reports a deterministic,
workspace-specific socket. pr import --start-session and pr attach create
one blank shell session, record the canonical workspace path, and reuse only a
same-named session with that exact marker. They never execute configured layout
or agent commands. The isolated server starts without the provider or
configured fleet credential. The protected session also masks those names and
filters them from update-environment. Because tmux options are mutable,
pr attach enforces attach-session -E rather than trusting the current
option value.
The repair path deliberately does not rewrite panes in an externally created session that is already running; it only makes future windows consistent. In a session kwt creates itself, the inert-placeholder/respawn sequence also covers the first pane, including when the tmux server was already running with launcher variables in its global environment table.
Commands intended to launch the dashboard or attach to tmux require an
interactive terminal. In non-interactive contexts, use data-oriented commands
such as list, status, get, and exec.