sidecar-jira
August 20, 2026 · View on GitHub
Always check if you are running in Sidecar: run sidecar --agents for capabilities.
Read-only Jira Cloud access for humans, agents, and Sidecar. This is the first Sidecar addition: a standalone executable that teaches Sidecar to recognize issue keys in terminal output and open them in a Resource pane.
It does not import Sidecar and Sidecar does not import it. The only coupling is
one JSON object in and one JSON object out, over
sidecar.terminal-resource/v1. If you are writing the next addition, start
here.
Install
Requires Go 1.26+.
go install github.com/marcus/sidecar-jira/cmd/sidecar-jira@latest
From a checkout:
go build -o ~/go/bin/sidecar-jira ./cmd/sidecar-jira
# or, with a version stamp:
make install VERSION=v0.1.0
sidecar-jira must be on PATH for Sidecar to spawn it.
Configure this tool
Configuration is owned by this binary, not by Sidecar. Credentials never enter
Sidecar config. The file lives at $XDG_CONFIG_HOME/sidecar-jira/config.json,
falling back to ~/.config/sidecar-jira/config.json:
{
"defaultProfile": "work",
"profiles": {
"work": {
"baseUrl": "https://haplab.atlassian.net",
"email": "you@example.com",
"projectKeys": ["HAP", "TANGLE", "HAPLAB"],
"tokenEnv": "JIRA_API_TOKEN"
}
}
}
projectKeys is an allowlist. Sidecar's matcher is generated from it, and a
locator outside it is refused without a network call. Keys are sorted
longest-first so HAP does not shadow HAPLAB.
The file never holds a credential. The API token is read from the environment
variable named by tokenEnv (default JIRA_API_TOKEN) and is never logged,
echoed, or written anywhere. Create one at
https://id.atlassian.com/manage-profile/security/api-tokens.
export JIRA_API_TOKEN=your_token_here
sidecar-jira doctor --json
doctor probes /myself (a rejected token is a clean 401 there) and then
each project key. Fix whatever it reports before wiring Sidecar up.
Use it directly
Agents and humans share this CLI. --json is the agent surface.
sidecar-jira issue show HAP-1 # human output
sidecar-jira issue show HAP-1 --json # agent output
sidecar-jira doctor
sidecar-jira version
--json prints {"ok":true,"resource":{…}} on success and
{"ok":false,"error":{"code":…}} on failure, with exit code 1. The CLI will
fetch any issue the token can see; the allowlist applies to Sidecar matching,
not to issue show.
Wire it into Sidecar
In ~/.config/sidecar/config.json (or , → Panels & Integrations):
{
"terminalResources": {
"providers": [
{
"id": "jira-work",
"command": ["sidecar-jira", "sidecar-provider", "--profile", "work"],
"passEnv": ["JIRA_API_TOKEN"],
"enabled": true,
"timeout": "10s"
}
]
}
}
passEnv is how the token reaches the child. Sidecar never accepts inline
secrets. Restart Sidecar, or reopen Configuration so it re-runs describe.
When an agent (or you) prints HAP-1, Sidecar underlines it. Click it, or:
sidecar open --provider jira-work HAP-1
The pane shows summary, type, status, assignee, priority, labels, and the
description as markdown. o opens the Jira page; r refreshes.
Protocol mode
sidecar-provider is the Sidecar surface. It reads one JSON request on stdin
and writes exactly one JSON response on stdout. It exits 0 for typed successes
and typed failures; a non-zero exit means the provider itself broke.
Diagnostics go to stderr only.
echo '{"protocol":"sidecar.terminal-resource/v1","method":"describe","instance":"jira-work"}' \
| sidecar-jira sidecar-provider --profile work
describe is purely local: it builds the matcher from projectKeys and never
touches the network. resolve fetches one issue, budgeting its HTTP call
inside the request's deadlineMs so a slow Jira comes back as a typed
unavailable ("Timed out talking to Jira") instead of the host killing the
process group.
Every typed error sets retryable honestly rather than by code, and
setupHint is always a single line.
The contract is documented in Sidecar: terminal-resource-provider-protocol.md.
Layout, if you are writing the next addition
| Package | Owns |
|---|---|
internal/protocol | Wire types. Transport-free. Copy this, or the field names, into the next provider. |
internal/config | Provider-owned profiles. No credentials on disk. Matcher generation. |
internal/jira | Narrow HTTP client and issue → resource mapping. |
internal/adf | Atlassian Document Format → safe markdown (links become visible text). |
internal/cli | Thin shells: issue show, doctor, sidecar-provider. |
The interesting rules, all of which the protocol requires:
- stdout is exactly one JSON object. No banner, no log, no second value.
- typed failure still exits 0; transport failure is the only non-zero.
describedoes no network I/O.resolvebudgets work insidedeadlineMsand returnsunavailablerather than being SIGKILLed.- credentials live in the environment, never in config, never in log lines.
retryableis per-response, not inferred from the code.setupHintandmessageare a single line.
Tests run against httptest with captured fixtures. No credentials, no
network, no Sidecar process.
Error codes
| Jira response | Protocol code |
|---|---|
| 200 | success |
404 with X-Seraph-LoginReason present and not OK | unauthorized |
| 404 otherwise | not_found |
| 401 | unauthorized |
| 403 | forbidden |
| 429 | rate_limited (honours Retry-After) |
| 5xx | unavailable |
| transport/DNS failure or timeout | unavailable |
| any other status, or an unparseable body | internal |
| bad or missing configuration, missing token | invalid_config |
unsupported protocol, unknown method, missing/malformed params, unknown matcher ID, malformed stdin | invalid_request |
The 404 split matters: Jira Cloud answers /rest/api/3/issue/{key} with 404,
not 401, when the credential itself is rejected. Without the Seraph header
check a dead token reports "this ticket does not exist".
Scope
Jira Cloud v3, API-token auth, one issue endpoint. No Data Center adapter, no
custom-field mapping, no OAuth, no configure command.
Tests
go test ./...
make check
Fixtures under internal/*/testdata were captured from a throwaway Jira site
with no production data.
License
MIT