sidecar-github

August 21, 2026 · View on GitHub

Read-only GitHub issues and pull requests for humans, agents, and Sidecar. Standalone executable; it does not import Sidecar and Sidecar does not import it.

An agent prints https://github.com/your-org/your-repo/pull/88 in a terminal pane. You click it in Sidecar and get the PR — title, open/draft/merged state, author, reviewers, labels, and body — in a resource tab, without leaving the terminal. The controlling document is docs/plans/active/sidecar-github.md.

Install

go install github.com/marcus/sidecar-github/cmd/sidecar-github@latest
# or from a checkout:
make install

Requires Go 1.26+. No runtime dependencies; gh is not needed at runtime.

Configure

The token never touches a file. It lives in an environment variable (default GITHUB_TOKEN; gh auth token prints one if you use the gh CLI). The config file only names it:

mkdir -p ~/.config/sidecar-github && $EDITOR ~/.config/sidecar-github/config.json
{
  "defaultProfile": "work",
  "profiles": {
    "work": {
      "allow": ["your-org", "marcus/sidecar"],
      "tokenEnv": "GITHUB_TOKEN"
    }
  }
}
  • allow is an allowlist of owner or owner/repo. It is the only input to the matcher patterns: an owner entry covers every repository under that owner. Anything outside it is refused locally, with no network call.
  • tokenEnv names the environment variable holding a classic or fine-grained PAT with read access (default GITHUB_TOKEN).
  • baseUrl optionally overrides the API root (https://api.github.com). M0 targets github.com; the field exists so a proxy needs no code change.
  • $XDG_CONFIG_HOME moves the file; it is $XDG_CONFIG_HOME/sidecar-github/config.json when set.

Check your setup end to end:

export GITHUB_TOKEN=$(gh auth token)
sidecar-github doctor            # human-readable check report
sidecar-github doctor --json     # same thing, structured
sidecar-github issue show marcus/sidecar#88 --json

Doctor probes the credential (GET /user) and then every allow entry (GET /users/{owner} or GET /repos/{owner}/{repo}), reporting one ok/FAIL line per step with a hint for each failure.

Wire into Sidecar

Add a terminal-resource provider instance to Sidecar's app config (~/.config/sidecar/config.json):

{
  "terminalResources": {
    "providers": [
      {
        "id": "github",
        "command": ["sidecar-github", "sidecar-provider", "--profile", "work"],
        "passEnv": ["GITHUB_TOKEN"],
        "enabled": true,
        "timeout": "10s"
      }
    ]
  }
}

passEnv forwards the variable by name; Sidecar never sees an inline secret and this repo's config never stores one. Then verify from Sidecar's side:

sidecar terminal-links list --describe --json
sidecar terminal-links check github --resolve marcus/sidecar#88 --json

Headless opening also works without a click, once the instance resolves:

sidecar open --provider github https://github.com/marcus/sidecar/pull/88

URL clicks

Sidecar's built-in link matchers keep precedence over external providers, so a plain GitHub URL currently opens the browser rather than a pane. The provider still recognizes URLs when resolving (see below), and owner/repo#88 references are matched directly in terminal text today. Teaching the host to yield allowlisted github.com URLs to this provider is specified as claimHosts in the plan and is pending host work; nothing in this repo blocks on it.

Locators

All of these resolve to the same canonical identity, marcus/sidecar#88, so URL forms and short forms merge into one tab:

FormExample
URLhttps://github.com/marcus/sidecar/pull/88
scheme-lessgithub.com/marcus/sidecar/issues/42
short refmarcus/sidecar#88

GitHub shares one number space across issues and pull requests, so .../issues/N and .../pull/N are the same resource. Bare #123 stays inert: there is no implicit current-repository context.

What a resolved pane shows: title; Issue / Pull request subtitle; status (Open · info, Draft · neutral, Merged · success, Closed · success for completed issues, Closed · danger for unmerged PRs, Closed · neutral when closed as not planned); fields for author, assignees, requested reviewers, labels, milestone, and comment count; the body as markdown; and GitHub's own html_url as the o action. Refreshes after 60 seconds of staleness.

Commands

sidecar-github issue show <LOCATOR> [--profile NAME] [--config PATH] [--json]
sidecar-github doctor               [--profile NAME] [--config PATH] [--json]
sidecar-github sidecar-provider     [--profile NAME] [--config PATH]
sidecar-github version | help

sidecar-provider is protocol mode for Sidecar (sidecar.terminal-resource/v1): one JSON request on stdin, exactly one JSON response on stdout, exit 0 for typed success and typed failure.

Errors and what they mean

Every failure is typed, carries an honest retryable flag, and ends with a one-line setup hint you can act on. The common ones:

SymptomCodeFix
No config file / bad JSONinvalid_configCreate the file; the hint contains a minimal working example
environment variable GITHUB_TOKEN is emptyinvalid_configexport GITHUB_TOKEN=$(gh auth token)
Locator outside the allow listnot_foundAdd the owner or owner/repo to allow; no network call was made
Token rejectedunauthorizedCheck/expiry-regenerate the token, then run sidecar-github doctor --json
Private repo, 404not_foundGitHub hides private repos from tokens without access; grant the PAT read access (fine-grained tokens need explicit per-repo grants)
Rate limitedrate_limited, retryableWait for the window named in the message; unauthenticated use exhausts fast
Forbidden despite valid authforbiddenFine-grained token lacks repository contents read; org SSO may need approval
Timeout / unreachable / 5xxunavailable, retryableCheck network and HTTPS_PROXY; Retry usually works

doctor reproduces most of these headlessly, which is the fastest way to see which layer failed.

Development

make check    # vet + tests
go test ./...

Tests need no network and no credentials; every HTTP path runs against httptest fixtures shaped like real GitHub payloads, and the process-contract tests build and drive the real binary the way Sidecar does.

Security posture

  • The token is read from an environment variable at call time. It is never written to disk, logged, echoed, or included in any error message (enforced by test).
  • Matching is local and offline; describe performs no I/O beyond reading the config file (enforced by test).
  • The provider is read-only: M0 speaks only GET to the REST API.