Pull-request automation¶
Use kwt to discover a GitHub pull request, import it into an isolated inert worktree, inspect the checkout, and attach through its protected tmux boundary. kwt preserves the contributor branch's exact push destination while keeping repository setup and agent commands stopped until you choose to run them.
Discover, import, inspect, and attach¶
List pull requests for a registered project:
Pass the selected ID back to kwt to create the worktree:
The result includes workspace.path. Review that exact checkout before running
project code:
Attach only through the protected PR command:
Direct kwt open and dashboard attachment refuse imported pull-request
workspaces. The protected command rechecks the live worktree and its recorded
project before it creates or repairs the isolated tmux session.
Automation contract¶
kwt owns GitHub discovery, Git refs, worktree naming, push routing, and protected session setup. A client should render the records returned by kwt and pass the selected pull-request ID back. It should not reproduce those rules itself.
kwt pr list and kwt pr import are noninteractive and always emit JSON. The
examples keep --json explicit to document that the caller depends on the
automation contract.
Clients that present their own direct tmux client can ask kwt to establish the canonical workspace session without attaching kwt's process:
Automation clients should bind the import to the project record that the user
selected. Supply the matching repository and registration_fingerprint
from kwt projects --json together:
kwt pr import 17 --project github.com/acme/widget \
--expected-repository <project.repository> \
--expected-registration <project.registration_fingerprint> \
--start-session --json
Kwt revalidates both values while holding the project lifecycle fence and
before creating the worktree. A changed or replaced project returns the
retryable registration_changed error without importing anything. The two
flags are an all-or-nothing contract; ordinary interactive use may omit both.
Every successful import response includes tmux_attach_mode: "protected".
When repository identity verifies the session name, it also includes the
deterministic tmux_socket_name. A protected result with no socket is protected
but unresolved: it is not the default tmux server, and clients must not attach
or create a session from that result. --start-session reports a
session_start_error for this state. kwt pr attach may resolve the endpoint
later, but only after it revalidates the live workspace provenance.
The socket name selects the endpoint; the attach mode selects Kwt's protected
attachment policy and must not be inferred from the presence of a named socket.
--start-session creates or repairs the returned session_name as a single
blank shell session and leaves it detached for the caller. It never runs
configured layouts, agent commands, or commands from the imported checkout.
Clients must attach through Kwt's protected path:
Automation clients bind session establishment to current kwt list --json and
kwt projects --json snapshots. The worktree list is flat and does not contain
a nested project record or registration fingerprint. Match its repository
field to the project record with the same repository, then supply these five
values together:
| Attach flag | Source |
|---|---|
--expected-repository |
Matching project's repository from kwt projects --json |
--expected-registration |
Matching project's registration_fingerprint from kwt projects --json |
--expected-generation |
Selected worktree's generation from kwt list --json |
--expected-session |
Selected worktree's session_name from kwt list --json |
--expected-socket |
Selected worktree's tmux_socket_name from kwt list --json |
kwt pr attach <workspace.path> \
--expected-repository <project.repository> \
--expected-registration <project.registration_fingerprint> \
--expected-generation <workspace.generation> \
--expected-session <workspace.session_name> \
--expected-socket <workspace.tmux_socket_name>
Kwt revalidates those values while holding the project lifecycle fence and
before it creates or repairs the protected tmux session. A changed project,
worktree incarnation, session name, or socket returns the retryable
registration_changed error without touching tmux. The five flags are an
all-or-nothing contract; ordinary interactive use may omit all of them.
kwt pr attach is an interactive exception to the JSON automation contract.
Validation and session-establishment failures that occur before attachment
still use the structured error envelope and stable PR exit status. On
Unix-like systems, a successful handoff replaces kwt with tmux; from that
point onward, tmux owns terminal output, signal handling, and the final exit
status. An immediate tmux client failure after replacement therefore uses
tmux's native diagnostic and status rather than a JSON envelope. Windows
retains a waiting kwt parent and can still wrap a returned tmux failure.
The attach command resolves the persisted workspace identity, verifies the
recorded project clone and exact live worktree identity, and creates or repairs
a single blank shell session on its isolated server before executing
attach-session -E so tmux cannot import client environment variables. It
never runs configured layout or agent commands. This makes the command converge
imports created without --start-session, imports whose startup failed, and
sessions that disappeared after import. A parent tmux client identity is
removed before attachment, so the command also works when invoked from a pane
connected to another tmux server. This nests the protected client; detaching
returns to the outer server, and the shared prefix may need to be sent twice to
reach the inner client. A
deleted project, reused path, or branch, repository, or session-name mismatch
fails closed. Prunable entries and paths without a live Git worktree are not
accepted. The registered project's canonical identity remains authoritative
when its checkout's origin points to a fork, matching kwt's other inventory
surfaces. An import created from an unregistered repository remains attachable
when its live Git identity matches the recorded repository; ambiguous or
conflicting registrations fail closed. Direct kwt open and dashboard open
actions refuse imported workspaces because they use the normal tmux server;
use kwt pr attach <workspace.path> instead. The protected attach path is
idempotent for an already imported worktree or a verified session that kwt
created for the same workspace. If runtime session establishment fails after
the import becomes durable, the command still exits successfully and returns
the imported workspace with an explicit
session_start_error:
{
"status": "created",
"workspace": {
"path": "/worktrees/pr-17",
"session_name": "kwt-wt-widget-pr-17-a1b2c3d4"
},
"session_start_error": {
"code": "workspace_creation_failed",
"message": "failed to start imported workspace session",
"retryable": false
}
}
Clients must retain or refresh the imported workspace when this field is
present and present the session failure separately. kwt pr attach
<workspace.path> retries session establishment directly; repeating the import
also converges on already_imported.
Kwt runs each protected PR workspace on a deterministic, workspace-specific
tmux socket rather than the shared dedicated kwt server or the
default server. Before invoking that server, kwt removes KWT_GITHUB_TOKEN,
KWT_FLEET_TOKEN, and the variable
named by fleet.token_env from the subprocess environment. It installs
matching session remove-markers before any imported-workspace shell starts.
Because tmux options are mutable by processes with socket access, filtering
update-environment is defense in depth; the protected attach command always
passes -E and never relies on that option for enforcement. Operational state
such as KWT_HOME remains available inside the workspace.
Kwt reuses an existing session on that isolated socket only when the session carries the matching workspace marker and both its server and session environments are credential-free. A rejected protected session must be removed before retrying.
kwt list --json reads the same provenance store before it labels worktrees
with tmux_socket_name and tmux_attach_mode. If that store cannot be read or
decoded, listing fails instead of emitting an imported workspace without its
safety-critical endpoint and attachment policy.
--project accepts a repository identity from kwt projects --json, a
registered project name, or its absolute canonical main-repository path.
Identity and unique-name matching take precedence over path matching; relative
and symlinked path selectors are rejected. --project may be omitted when the
command runs inside the desired repository. If a display name identifies
multiple projects, kwt returns repository_mismatch; callers must use the
repository identity or path. --state accepts open, closed, or all and
defaults to open.
Before listing or importing pull requests, kwt resolves the selected repository through GitHub and uses the current canonical identity for the provider request, workspace, and provenance. GitHub repository transfers and renames therefore continue to work when the project registry still contains the previous identity. This resolution is operation-local and does not rewrite the registered project, whose identity may intentionally name an upstream repository while the checkout's origin points to a fork. Import selectors may use either the registered or resolved repository URL during that transition. Existing imports from the same project clone remain discoverable through the verified alias pair; the next import migrates their repository, project, same-repository source, workspace, and record-key identities to the resolved repository atomically. Provenance retains the canonical alias history so later transfers remain connected through a previously verified identity. Protected attachment requires that history to overlap the live registered identity and validates the recorded deterministic session against its own historical repository identity.
Authentication¶
GitHub API authentication is resolved without prompting:
KWT_GITHUB_TOKEN, when nonempty.- The output of
gh auth token.
kwt uses that token only through go-github; it never writes or prints the
token. Git fetch and push use normal Git remote authentication. Import fetches
with GIT_TERMINAL_PROMPT=0, so missing Git credentials fail instead of
blocking an embedded or SSH client. Configure a Git credential helper or SSH
authentication for subsequent pushes.
Import stops before mutation when a repository stores credentials directly in
a remote fetch or push URL. Use a Git credential helper or SSH agent instead;
this keeps contributor-triggered Git operations from reading reusable
credentials out of the linked worktree's shared config.
Validation includes configured include files and checks Git's effective fetch
and push URLs after insteadOf and pushInsteadOf rewriting. Remote URLs with
query strings or fragments are rejected, as are invalid scheme-based URLs and
opaque remote-helper (transport::address) URLs.
Import fetches also force the SSH implementation's noninteractive mode (OpenSSH batch mode or PuTTY/plink's equivalent) and disable askpass-style credential prompts. Every ref-mutating import operation—including fetch, checkout, and rollback—runs with the same sanitized environment and an empty trusted hooks directory. Checkout additionally disables every configured smudge/process filter. Repository hooks, filters, copied files, and setup commands therefore do not run during PR import: environment scrubbing alone cannot prevent same-user processes from reading kwt configuration or token files from disk.
Kwt requires Git 2.20 or newer. PR import uses per-worktree Git configuration
to make plain git push target the PR head without changing push behavior in
the main checkout, and checks that capability before it fetches refs, adds
remotes, or creates a worktree. PR import also requires Git 2.42.0 or newer on
macOS and Linux, or Git for Windows 2.53.0.windows.3 or newer, for its safe
merge-driver behavior. The kwt doctor command and explicit kwt prune
policies require Git 2.31 or newer for their worktree inventory and repair
operations.
Listing contract¶
{
"pull_requests": [
{
"id": "github:github.com/acme/widget#17",
"provider": "github",
"repository": {
"provider": "github",
"identity": "github.com/acme/widget",
"host": "github.com",
"owner": "acme",
"name": "widget"
},
"number": 17,
"url": "https://github.com/acme/widget/pull/17",
"title": "Improve widget rendering",
"author": "octocat",
"source": {
"branch": "feature/rendering",
"repository": {
"provider": "github",
"identity": "github.com/octocat/widget",
"host": "github.com",
"owner": "octocat",
"name": "widget"
},
"is_fork": true
},
"target": {
"branch": "main",
"repository": {
"provider": "github",
"identity": "github.com/acme/widget",
"host": "github.com",
"owner": "acme",
"name": "widget"
},
"is_fork": false
},
"draft": true,
"state": "open",
"head_sha": "0123456789abcdef0123456789abcdef01234567",
"imported": false
}
]
}
The opaque id is stable for the provider, base repository, and PR number.
Import also accepts a PR URL or a number scoped by --project.
An imported list result adds the canonical workspace record:
{
"id": "github:github.com/acme/widget#17",
"provider": "github",
"repository": {
"provider": "github",
"identity": "github.com/acme/widget",
"host": "github.com",
"owner": "acme",
"name": "widget"
},
"number": 17,
"url": "https://github.com/acme/widget/pull/17",
"title": "Improve widget rendering",
"author": "octocat",
"source": {
"branch": "feature/rendering",
"repository": {
"provider": "github",
"identity": "github.com/octocat/widget",
"host": "github.com",
"owner": "octocat",
"name": "widget"
},
"is_fork": true
},
"target": {
"branch": "main",
"repository": {
"provider": "github",
"identity": "github.com/acme/widget",
"host": "github.com",
"owner": "acme",
"name": "widget"
},
"is_fork": false
},
"draft": false,
"state": "open",
"head_sha": "0123456789abcdef0123456789abcdef01234567",
"imported": true,
"workspace": {
"id": "github.com/acme/widget:pr-17-feature-rendering:a1b2c3d4",
"repository": "github.com/acme/widget",
"branch": "pr-17-feature-rendering",
"path": "/home/alice/.kwt/worktrees/github.com/acme/widget/pr-17-feature-rendering",
"state": "ready",
"session_name": "kwt-wt-widget-pr-17-feature-rendering-a1b2c3d4"
}
}
Import contract¶
kwt chooses a deterministic local branch name for the initial import and
delegates remote selection, fetch, no-checkout worktree creation,
materialization, and push routing to Kit. If the recorded worktree was removed
but its branch was preserved, another import creates a disambiguated branch
and updates the PR's provenance to the new worktree. The preserved branch is
left untouched. When the source branch is reachable, plain git push updates
exactly the PR's original head branch. If Kit cannot establish that exact fork
tracking, or KWT cannot validate it, import fails and rolls back the worktree
instead of leaving a checkout whose plain push could fall back to the base
repository. Unlike an ordinary kwt add, PR import does not apply copy_files
or setup_commands; run any desired project setup explicitly after reviewing
the imported files.
When Kit creates a fork remote, it preserves the project's working push
authentication transport, including SSH host aliases and explicit ports. KWT
then verifies the effective push destination and rejects broader push behavior
before reporting success. Import reports the exact tmux session name a client
can attach to; it does not launch or manipulate tmux panes.
Cross-project imports load the selected project's already trusted .kwt.toml
in isolation. They never load configuration from the caller's working
directory and never prompt or auto-trust in this automation path. Repository
copy_files and setup_commands are deliberately ignored for PR imports. A
target repository's .kwt.toml must be a
regular file, not a symlink, so trust granted to another path cannot be reused.
The registered project path must resolve to that repository's main Git root;
empty, relative, missing, subdirectory, and linked-worktree paths are rejected
before target configuration is loaded.
Configured file copies use rooted destination operations and reject symlinks
in the destination path, preventing contributor-controlled checkout entries
from redirecting writes outside the new worktree. Relative paths in a trusted
target .kwt.toml are resolved against that target repository, never the
caller's working directory, and target-local path fields cannot expand
environment variables into workspace paths. Naming output influenced by
target-local configuration is not environment-expanded after rendering.
Before materializing pull-request files, kwt creates a no-checkout worktree and
verifies that branch- and worktree-conditional Git includes do not change its
effective configuration, including record order and precedence. Push URLs and
refspecs are validated against the PR source repository again after push
configuration, and import fails if HEAD no longer names the generated
workspace branch. Worktree paths are stored and matched in canonical form so
symlinked base directories do not create duplicate imports.
A new import returns:
{
"status": "created",
"pull_request": {
"id": "github:github.com/acme/widget#17",
"provider": "github",
"repository": {
"provider": "github",
"identity": "github.com/acme/widget",
"host": "github.com",
"owner": "acme",
"name": "widget"
},
"number": 17,
"url": "https://github.com/acme/widget/pull/17",
"title": "Improve widget rendering",
"author": "octocat",
"source": {
"branch": "feature/rendering",
"repository": {
"provider": "github",
"identity": "github.com/octocat/widget",
"host": "github.com",
"owner": "octocat",
"name": "widget"
},
"is_fork": true
},
"target": {
"branch": "main",
"repository": {
"provider": "github",
"identity": "github.com/acme/widget",
"host": "github.com",
"owner": "acme",
"name": "widget"
},
"is_fork": false
},
"draft": false,
"state": "open",
"head_sha": "0123456789abcdef0123456789abcdef01234567",
"imported": true,
"workspace": {
"id": "github.com/acme/widget:pr-17-feature-rendering:a1b2c3d4",
"repository": "github.com/acme/widget",
"branch": "pr-17-feature-rendering",
"path": "/home/alice/.kwt/worktrees/github.com/acme/widget/pr-17-feature-rendering",
"state": "ready",
"session_name": "kwt-wt-widget-pr-17-feature-rendering-a1b2c3d4"
}
},
"project": {
"identity": "github.com/acme/widget",
"name": "widget",
"path": "/home/alice/src/widget"
},
"workspace": {
"id": "github.com/acme/widget:pr-17-feature-rendering:a1b2c3d4",
"repository": "github.com/acme/widget",
"branch": "pr-17-feature-rendering",
"path": "/home/alice/.kwt/worktrees/github.com/acme/widget/pr-17-feature-rendering",
"state": "ready",
"session_name": "kwt-wt-widget-pr-17-feature-rendering-a1b2c3d4"
}
}
Repeating the same import returns the same shape and workspace with
"status": "already_imported". Provenance is stored in
$KWT_HOME/pull-requests.json (or ~/.config/kwt/pull-requests.json) and is
updated under a cross-process file lock. The lock covers checking, fetching,
creating, configuring, and recording, so concurrent imports converge on one
workspace. A stale provenance record is not reported as imported when its Git
worktree no longer exists. Existing imports require complete source provenance
and matching project-clone, live worktree path, and branch identities before
already_imported is returned. Importing the same PR from another clone
returns a conflict rather than replacing the original clone's provenance. KWT
does not push during this check or rewrite Git remotes and routing that the
local user changed after import.
If the import transaction fails after creating a workspace, kwt rolls it back
even when the request context was canceled. Session establishment happens
after that transaction and therefore uses the partial-success contract above
rather than claiming rollback. Request cancellation, including SIGINT and
SIGTERM, terminates checkout; cleanup then runs without the canceled context.
Worktree creation retains an ownership reservation through late rollback and
removes only the original directory identity, a clean worktree, and the
unchanged reserved ref. A replaced path, dirty worktree, or advanced branch is
preserved and reported for manual cleanup. If the PR's recorded source
repository or branch changed while its imported workspace is still present,
another import returns import_conflict.
Failure contract¶
Failures from kwt pr list, kwt pr import, and the validation or session
establishment phase of kwt pr attach write a JSON error to stdout, a
credential-free diagnostic to stderr, and return a stable nonzero status. The
Unix attachment handoff is the exception described above: after process
replacement succeeds, tmux owns any later diagnostic and exit status. For
example:
{
"error": {
"code": "authentication_failed",
"message": "GitHub authentication failed",
"retryable": false
}
}
| Exit | Error code | Meaning |
|---|---|---|
| 2 | invalid_pull_request_selector |
Invalid state, URL, opaque ID, or number. |
| 3 | authentication_failed |
GitHub API or Git authentication failed. |
| 4 | repository_mismatch / unsupported_provider |
Project selection or provider mismatch. |
| 5 | pull_request_not_found |
The selected PR or repository is missing. |
| 6 | inaccessible_head |
The fork or source branch is unavailable. |
| 7 | naming_conflict |
The generated branch or workspace is occupied. |
| 8 | network_failure |
A retryable provider or Git network failure. |
| 9 | workspace_creation_failed |
Worktree creation, setup, push config, persistence, or session-configuration preflight failed. |
| 10 | malformed_provider_response |
GitHub returned an invalid success response. |
| 11 | import_conflict |
Concurrent state or the selected head SHA changed. |
| 12 | unsupported_git_version |
Git is too old for isolated per-worktree push configuration. |
GitHub primary and secondary rate limits, including HTTP 429 responses, use
network_failure with retryable: true.
All diagnostics go to stderr. Consumers should parse stdout and branch on
error.code; they never need to scrape CLI prose.