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"
}
}
}
allowis an allowlist ofownerorowner/repo. It is the only input to the matcher patterns: anownerentry covers every repository under that owner. Anything outside it is refused locally, with no network call.tokenEnvnames the environment variable holding a classic or fine-grained PAT with read access (defaultGITHUB_TOKEN).baseUrloptionally overrides the API root (https://api.github.com). M0 targets github.com; the field exists so a proxy needs no code change.$XDG_CONFIG_HOMEmoves the file; it is$XDG_CONFIG_HOME/sidecar-github/config.jsonwhen 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:
| Form | Example |
|---|---|
| URL | https://github.com/marcus/sidecar/pull/88 |
| scheme-less | github.com/marcus/sidecar/issues/42 |
| short ref | marcus/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:
| Symptom | Code | Fix |
|---|---|---|
| No config file / bad JSON | invalid_config | Create the file; the hint contains a minimal working example |
environment variable GITHUB_TOKEN is empty | invalid_config | export GITHUB_TOKEN=$(gh auth token) |
| Locator outside the allow list | not_found | Add the owner or owner/repo to allow; no network call was made |
| Token rejected | unauthorized | Check/expiry-regenerate the token, then run sidecar-github doctor --json |
| Private repo, 404 | not_found | GitHub hides private repos from tokens without access; grant the PAT read access (fine-grained tokens need explicit per-repo grants) |
| Rate limited | rate_limited, retryable | Wait for the window named in the message; unauthenticated use exhausts fast |
| Forbidden despite valid auth | forbidden | Fine-grained token lacks repository contents read; org SSO may need approval |
| Timeout / unreachable / 5xx | unavailable, retryable | Check 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;
describeperforms no I/O beyond reading the config file (enforced by test). - The provider is read-only: M0 speaks only
GETto the REST API.