Skip to content

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:

kwt pr list --project github.com/acme/widget --state open --json

Pass the selected ID back to kwt to create the worktree:

kwt pr import github:github.com/acme/widget#17 \
  --project github.com/acme/widget --json

The result includes workspace.path. Review that exact checkout before running project code:

kwt changes /path/from/import-result

Attach only through the protected PR command:

kwt pr attach /path/from/import-result

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:

kwt pr import 17 --project github.com/acme/widget \
  --start-session --json

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:

kwt pr attach <workspace.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:

  1. KWT_GITHUB_TOKEN, when nonempty.
  2. 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.