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

PackageOwns
internal/protocolWire types. Transport-free. Copy this, or the field names, into the next provider.
internal/configProvider-owned profiles. No credentials on disk. Matcher generation.
internal/jiraNarrow HTTP client and issue → resource mapping.
internal/adfAtlassian Document Format → safe markdown (links become visible text).
internal/cliThin 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.
  • describe does no network I/O.
  • resolve budgets work inside deadlineMs and returns unavailable rather than being SIGKILLed.
  • credentials live in the environment, never in config, never in log lines.
  • retryable is per-response, not inferred from the code.
  • setupHint and message are a single line.

Tests run against httptest with captured fixtures. No credentials, no network, no Sidecar process.

Error codes

Jira responseProtocol code
200success
404 with X-Seraph-LoginReason present and not OKunauthorized
404 otherwisenot_found
401unauthorized
403forbidden
429rate_limited (honours Retry-After)
5xxunavailable
transport/DNS failure or timeoutunavailable
any other status, or an unparseable bodyinternal
bad or missing configuration, missing tokeninvalid_config
unsupported protocol, unknown method, missing/malformed params, unknown matcher ID, malformed stdininvalid_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