ProjectOperations CLI · 2026
Jira CLI
A fast command-line interface for Jira issues, JQL, transitions, boards and sprints. Triage and bulk updates at the speed of typing rather than clicking.
- Go
- Jira REST API
- JSON
jira issue search "statusCategory != Done" -o json{"total":7,"issues":[{"key":"ABC-141","summary":"Fix CI"… jira issue create -p ABC --type Task --summary "Set up CI"ABC-142 created jira issue transition ABC-142 "In Progress"ABC-142 To Do -> In ProgressOperations CLI · Project
Jira CLI is a Go binary over the Jira REST API, built for the triage I used to do by clicking through the browser. It covers issues, JQL search, transitions, comments, worklogs, attachments, links, watchers and votes, alongside projects, users, fields, boards, sprints and epics. It works against Jira Cloud and against Server or Data Center.
Two things make Jira awkward to script. Authentication differs by deployment, so a tool that only speaks Cloud API tokens is useless against a self-hosted instance. And descriptions are Atlassian Document Format, not text, so anything that writes a description has to build a document tree by hand.
Where the friction was
I was repeating the same three moves every morning. Find what I owned and had not finished, move a ticket, leave a comment. Each one meant a browser tab, a filter rebuilt from scratch, and a dropdown to hunt for. Raw curl was not the answer either. Cloud paginates issue search with nextPageToken on /search/jql, while Server and Data Center still use startAt on the classic /search. Agile boards sit on a separate /rest/agile/1.0 API. Writing that once is fine. Writing it into every script is not.
Inside the binary
The repository is a monorepo. The binary lives in cli-go/ and the documentation site in web/. Commands sit under cli-go/cmd/, one file per resource group, over a REST layer in cli-go/internal/client/. Configuration resolves in one order: command flags, then environment variables, then the stored profile. Profiles live in ~/.config/jira-cli/config.yaml, XDG aware and written at mode 0600. A file lock wraps every load, modify and save cycle, so a rotated OAuth refresh token survives two processes refreshing at the same moment. internal/adf/ builds the document tree that rich text fields require, and internal/output/ renders table, JSON or YAML behind one formatter interface.
The command groups
| Group | Commands |
|---|---|
| Identity | auth, whoami, status, version |
| Issues | issue, with comment, worklog, attach, link, watch and vote verbs |
| Metadata | project, user, field, component, status, filter |
| Agile | board, sprint, epic |
| Administration | permission, group, webhook, release, dashboard |
Every group reads as resource then verb, and every list or get command takes -o table, -o json or -o yaml. Users are account IDs under GDPR, so commands accept an email, a display name, @me or id:<accountId> and resolve it before use.
Choices and their cost
- Every authentication method is supported, not only the Cloud token. OAuth 2.0 3LO, Cloud API token, scoped token, Server personal access token and basic credentials all work. The cost is a login command with five branches, and OAuth needs granular
jira-softwarescopes before board and sprint commands stop failing with 401. issue createreuses a recent match. Before writing it looks for an issue by the same user, in the same project, with the same type, summary and parent, created in the last ten minutes. A retrying agent is therefore safe. Two genuinely identical tickets now need--allow-duplicate.- Mutating POST requests are never retried.
PostJSONsends once, andPostJSONRetryableexists only for the search shaped calls. An ambiguous response fails loudly instead of filing a second ticket. - Release binaries carry no OAuth client secret, so each user registers their own 3LO app. This is the decision I would revisit. A server side authorization broker would remove that setup step without shipping a reusable secret in a public binary.
A triage pass
export JIRA_SITE=https://acme.atlassian.net
export [email protected] JIRA_TOKEN=xxxx
jira status
jira issue search "assignee = currentUser() AND statusCategory != Done" -o json | jq -r '.[].key'
jira issue transitions ABC-142
jira issue transition ABC-142 "In Progress"
jira issue comment ABC-142 --markdown --body "Picking this up"
No auth login step is needed when the environment already carries credentials. status checks reachability, authentication and server info in one call. Transitions are discover then do, because Jira has no set status operation. The CLI lists what the workflow allows, then matches the argument against a transition name or its target status. Informational lines go to stderr, which is what makes the jq above safe. Unattended, JIRA_READ_ONLY=1 JIRA_NO_INPUT=1 blocks writes and removes prompts.
Where it stops
This is an independent wrapper over the REST API. It is not affiliated with Atlassian and unrelated to the official acli. JQL has to be bounded, since search is not a crawler. Agile commands over OAuth need scopes the classic presets do not include, so a token or a personal access token is the shorter path there. Basic authentication against Server can trip CAPTCHA lockout. Building from source needs Go 1.26 or later. It is MIT licensed, ships binaries for macOS, Linux and Windows on amd64 and arm64, and GoReleaser cuts a release on any signed v* tag.
Next projectOperations CLI