0084: Chat reads pull requests through your gh

On this page

Status: accepted

Context

When someone asks about their changes, the useful question is often about the pull request, not the working tree: what reviewers said, which checks failed, what's in it. That lives on GitHub, which needs the person's credentials. Short Leash keeps no secrets in its config (invariant 9), and chat never sends data out (invariant 15, 0062). Developers already sign in to GitHub's own command, gh. This is the first chat tool that reaches the internet and the first time Short Leash runs another program with the user's credentials, so it's recorded apart from the on-device model work in 0080.

Decision

  • The user's own gh, read-only. Short Leash runs the gh it finds on the login shell's PATH, as it finds the programs a site runs, and only gh pr view --json … and gh pr diff (changes::pull_request). The repository is named with --repo owner/name, taken from the GitHub address of the branch's push remote (its pushRemote, remote.pushDefault, the remote it tracks, else origin), then, for a fork, a remote named upstream, asked for owner:branch. The branch goes after --. gh runs in Short Leash's own folder, never the repository's, so it doesn't read the repository's Git config or run Git in it. Never gh api, gh auth token or anything that posts.
  • An environment built from nothing. gh gets only an allowlist from leashd's environment (GH_PASS_ENV: where its sign-in and config are, the proxy and certificates, and what Windows needs to start a program), so no GH_TOKEN, GITHUB_TOKEN, GH_REPO, GH_HOST, debugging or browser variables reach it. Then fixed values (GH_FIXED_ENV): GH_HOST=github.com, and prompts, pagers, color, the spinner and update notices off. It's stopped after 20 seconds (GH_TIMEOUT) or once it prints more than 8 MiB (GH_MAX_OUTPUT), and on Windows it starts without a console window. Short Leash never reads gh's files, never asks it for a token and never stores one: gh reads its own sign-in from the Keychain or Credential Manager.
  • Off until the person turns it on. Let chat read pull requests from GitHub in Settings › Chat ([ai] chat_pull_requests, off by default). The engine checks it before anything runs, before even looking for gh (Engine::read_pull_request_on), so chat, leash mcp and the Changes page share one switch, and no tool can change it. One switch, because the data is the person's own pull request, and an assistant's question must never take Short Leash online without their choice. When chat asks while it's off, the read's row says so with Turn on: that row comes from the engine, never the model, and turning on goes through a confirm that says what goes online, with Cancel focused.
  • The answer is outside text. Every text is capped (title 300 characters, description 8,000, the latest 30 reviews and 30 comments of 2,000 each, the first 300 files) and has invisible characters removed; file paths and diffs show them as codes such as ⟦U+200B⟧ instead (parse_view, the pull_request tool). Open on GitHub is built from the repository and number, never from the address in the answer. Over MCP the answer carries outside_text: true and an about note; in chat the read marks the chat Has outside text. Chat sizes it to the model like every diff read (0080).
  • Kept a minute, in memory. leashd reuses a pull request for 60 seconds (PULL_REQUEST_FRESH_FOR) when asked again about the same repositories and branch, unless Refresh asks again; a read that fails, and turning the setting off, forget them. Nothing is written to disk beyond what a chat already keeps (its messages, 90 days).
  • GitHub only, and only what gh pr view shows. github.com, not other hosts. Comments on single lines need gh api and aren't read.
  • How invariant 15 reads. This tool sends GitHub only the repository, the branch (owner:branch for a fork) and, for a diff, the pull request's number, under the person's own sign-in, after they turned it on. It's the one exception to "chat never sends data out", and CLAUDE.md's invariant 15 names it. It reads and never changes anything, so it's read, not never_in_chat.

Consequences

  • Short Leash handles no GitHub token, and needs no OAuth app of its own.
  • It needs gh installed and signed in; each failure says what to run (gh auth login, gh auth status) or to check the connection.
  • An answer can be a minute old.
  • GitHub Enterprise and other hosts aren't covered, and an organization that blocks gh blocks this too.
  • A pull request's text is written by other people. It stays data with the outside text mark, and chat can't act on it without asking (0062).