jiro is a Jira CLI for humans, scripts, and AI agents. It is built for Jira
Data Center and Server, with readable text for interactive use and stable,
versioned contracts for automation.
- Work with Issues, comments, links, transitions, Sprints, Projects, and Custom Fields from one consistent CLI.
- Search with focused flags or JQL, and run guarded bulk operations with dry-run support and explicit confirmation.
- Read terminal-aware tables interactively or pipe complete, headerless TSV to other tools.
- Use normalized JSON, stable exit codes, and
jiro schemafor scripts and agents. - Keep Basic Auth and PAT Credentials in Profile-scoped OS keyring entries, with read-only Profiles for safer automation.
- Convert Markdown descriptions and comments to Jira Markup while keeping Jira Markup as the explicit default.
- Drop down to
jiro apifor authenticated raw Jira REST requests when a typed command is not enough.
jiro is under initial development. The v1 compatibility target is Jira Data Center and Server REST API v2. Jira Cloud REST API v3 and ADF are not part of the compatibility contract.
GitHub Releases provides standalone binaries for Linux, macOS, and Windows on amd64 and arm64. Download the asset for your operating system and architecture. On Linux and macOS, make it executable and optionally rename it:
chmod +x jiro_v0.1.0_darwin_arm64
mv jiro_v0.1.0_darwin_arm64 jiro
./jiro --versionEach release includes a SHA-256 checksum file. Verify the matching entry before installing the binary. For example, on macOS:
grep 'jiro_v0.1.0_darwin_arm64$' jiro_v0.1.0_checksums.txt | shasum -a 256 -c -On Linux, use sha256sum -c - instead of shasum -a 256 -c -.
Log in to the default Profile, verify the Credential, and list some Issues:
jiro auth login
jiro auth status
jiro issue list --project OPS --status "In Progress"
jiro issue show OPS-42Use --profile when you need a named Profile:
jiro --profile bot auth login
jiro --profile bot auth statusThe default config path is $XDG_CONFIG_HOME/jiro/config.toml, falling back to
~/.config/jiro/config.toml.
jiro auth login prompts for connection settings and a fresh Credential. It
verifies the Credential with Jira before updating the Profile. A new Profile
must select basic or pat; jiro does not guess an authentication type.
jiro auth login
jiro --profile bot auth loginCredentials are stored in the OS keyring by default, with one independent keyring entry per Profile. To store the Credential in TOML, disable the keyring explicitly:
jiro auth login --use-keyring=falseOn Unix-like systems, jiro requires a config file containing password or
token to have mode 0600. This applies to files written by
auth login and to manually maintained TOML.
When use_keyring = true, a missing keyring entry is an error. jiro does not
fall back to a plaintext secret in the config file.
You may maintain the config file manually:
[default]
host = "https://jira.example.com"
username = "timon"
auth_type = "basic"
api_version = 2
use_keyring = true
user_agent = "jiro-automation/1"
[profiles.bot]
host = "https://jira.example.com"
auth_type = "pat"
use_keyring = true
read_only = true
user_agent = "jiro-bot/1"Select a Profile with --profile bot or JIRO_PROFILE=bot. --config and
--profile are the only global config-selection flags; connection and
authentication overrides use environment variables.
Supported environment variables include:
JIRA_HOST,JIRA_API_VERSION, andJIRA_USER_AGENTJIRA_TOKENfor PAT, or the atomicJIRA_USERNAMEandJIRA_PASSWORDpair for Basic AuthJIRO_PROFILE,JIRO_CONFIG_FILE, andJIRO_CONFIGJIRO_READ_ONLYandJIRO_USE_KEYRINGJIRO_FORCE_TTYfor terminal-style text tables through a pipe
A non-empty JIRA_TOKEN selects PAT and ignores the Basic Auth variables.
Without a token, setting either JIRA_USERNAME or JIRA_PASSWORD requires
both to be non-empty and selects Basic Auth.
If no environment Credential is present, jiro uses the selected Profile's
complete Credential. It never combines environment and persisted Credential
halves. JIRA_HOST is independent, so it can override the Jira Instance while
the selected Profile supplies its Credential.
JIRA_USER_AGENT overrides the selected Profile's user_agent, which
inherits from [default] when omitted. The built-in fallback is
jiro/<version>, or jiro/dev for development builds.
The legacy JIRO_HOST, JIRO_API_VERSION, JIRO_TOKEN,
JIRO_USERNAME, JIRO_PASSWORD, and JIRO_AUTH_TYPE variables are
ignored.
Provide a complete environment Credential or use an explicit stdin mode.
--password-stdin requires JIRA_USERNAME. --token-stdin selects PAT
and ignores the username.
The two stdin modes are mutually exclusive and conflict with a non-empty
JIRA_TOKEN or JIRA_PASSWORD. Each mode reads to EOF, removes exactly one
trailing LF and an immediately preceding CR, and rejects only an empty result.
Secrets are never accepted as command-line arguments.
printf '%s' "$JIRA_PAT" | JIRA_HOST=https://jira.example.com \
jiro --profile bot auth login --token-stdin
printf '%s' "$JIRA_PASSWORD" | JIRA_HOST=https://jira.example.com \
JIRA_USERNAME=timon jiro auth login --password-stdinauth status verifies the effective Profile and Credential with
/rest/api/2/myself:
jiro auth status
jiro --profile bot auth statusIt exits successfully only when Jira accepts the Credential. Its normalized output includes the Profile, Jira Instance, authentication type, and Principal without exposing the secret or its storage source.
auth logout removes a Profile's persisted Credential without deleting its
non-secret configuration:
jiro auth logout
jiro --profile bot auth logoutauth logout cannot unset JIRA_PASSWORD or JIRA_TOKEN inherited from
the shell. It reports when one of those environment Credentials remains active.
Resource namespaces use singular canonical names without aliases:
jiro issue
jiro project
jiro field
jiro issue list --project OPS --status "In Progress"
jiro issue list --resolution unresolved --reporter me --label agent --component API
jiro issue list --sprint active --created -7d --updated 'startOfWeek()'
jiro issue show OPS-42
jiro search 'project = OPS AND assignee = currentUser() ORDER BY updated DESC'Issue list filters are combined with AND. Repeated --label,
--component, and --fix-version values use JQL IN.
--resolution unresolved selects Issues without a resolution.
--assignee me and --reporter me use Jira's currentUser().
For issue list, Sprint accepts active or open, closed, future,
an ID, or a name. An absolute --created or --updated date
(YYYY-MM-DD or YYYY/MM/DD) selects that whole day. Relative values such
as -7d and allowlisted Jira date functions such as startOfWeek() select
values on or after that operand.
jiro issue add --project OPS --type Bug --summary "Broken deployment" \
--parent OPS-10 --component API --fix-version 4.5 --sprint active
jiro issue update OPS-42 --priority High --component API --fix-version 4.5
jiro issue update OPS-42 --component none --sprint active
jiro issue comment add OPS-42 --body "Deployed to staging."
jiro issue move OPS-42 --to "In Review"
jiro issue assign OPS-42 --assignee meissue update --component and --fix-version replace the complete field.
A single none clears it.
issue add --sprint creates the Issue before moving it into the Sprint. A
failed move does not delete the new Issue. issue update --sprint resolves
the Sprint before writing, updates ordinary fields, and then moves the Issue. A
failed Sprint move after an ordinary field update is reported as a partial
failure.
Write-time Sprint specs accept a numeric ID, active, or a case-insensitive
name substring. jiro uses the first match in Jira board and page order.
jiro issue link add OPS-42 --to OPS-99 --type Blocks
jiro issue link list OPS-42
jiro issue link delete 10001
jiro issue link types
jiro project list
jiro field list --custom
jiro cache refreshBulk operations select Issues with JQL. Exactly one of --dry-run and
--yes is required. --dry-run preflights every matching Issue;
--yes performs serial writes without prompting. Bulk commands page through
all matches by default.
jiro issue bulk move --jql 'project = OPS AND status = Open' --to "In Progress" --dry-run
jiro issue bulk assign --jql 'project = OPS' --assignee me --yesText is the default output format. Normalized JSON is the automation contract,
and jiro schema describes that contract for agents and other tooling.
Commands with tabular text output adapt to stdout:
- A terminal receives a column-aligned table sized to the available width.
Fixed Issue Key, ID, Alias, and similar columns remain complete. Descriptive
columns may be truncated with
.... - A pipe or file receives headerless TSV with untruncated, single-line values. An empty result writes zero bytes.
Table cells are rendered as safe single-line text. JSON retains the original values.
jiro issue show is the detail-view exception. Text output uses man-page-style
sections with uppercase field headers and four-space-indented values. TTY
headers are bold; piped or redirected headers contain no ANSI styling. Values
retain physical line breaks and are never wrapped or truncated by jiro. Empty
fields remain visible. JSON output is unchanged.
Set JIRO_FORCE_TTY to 1, true, yes, or on to keep
terminal-style output in a pipeline:
JIRO_FORCE_TTY=1 jiro issue list --project OPS | lessThe exact values 0, false, no, and off leave normal terminal
detection in place. Any other value is a configuration error for text output.
When forced, jiro reads the controlling terminal width and falls back to 80
columns if the width is unavailable.
The short and long output flags accept attached or separate values:
jiro issue list -o json
jiro issue list -ojson
jiro issue list --output json
jiro issue list --output=jsonNormalized JSON uses a stable envelope:
{"schemaVersion":"1","data":{"issues":[],"total":0,"startAt":0,"maxResults":50}}Successful commands can include structured warnings without changing the exit code:
{"schemaVersion":"1","data":{"key":"OPS-42","updated":true},"warnings":[{"code":"stale_field_cache","message":"using stale custom field metadata","details":{"fetchedAt":"2026-07-29T10:00:00Z"}}]}Successful data is written to stdout. Structured JSON failures are written to stderr with stable exit codes, while human-readable warnings use stderr.
For a partial result with exit code 7, jiro writes the complete normalized
result to stdout before writing a structured partial_failure error to
stderr. This includes a successful Issue creation whose Sprint move failed and
bulk runs with failed or unattempted items.
jiro schema describes automation-facing commands, flags, mutation status,
output shapes, and exit codes as machine-readable JSON. Shell completion emits
shell code and is outside that contract.
Pass custom fields with repeatable --field key=value flags:
jiro issue add \
--project OPS \
--type Story \
--summary "Agent-friendly output" \
--field story-points=5 \
--field customfield_10006='{"id":"123"}'A Custom Field ID such as customfield_10006 is used as-is and has priority.
It does not call /myself, read the cache, or query Jira field metadata.
A Custom Field Alias such as story-points is resolved through a 24-hour Field
Metadata Snapshot. The snapshot is scoped to the normalized Jira base URL and
the Principal returned by /myself. Missing or ambiguous aliases are errors.
Values are decoded as JSON first and fall back to strings.
The disposable JSON snapshots live under github.com/adrg/xdg's
xdg.CacheHome at:
jiro/fields/hosts/<url-slug+hash>--<principal-slug+hash>.json
This honors the platform XDG cache location, including XDG_CACHE_HOME where
applicable. macOS defaults to
~/Library/Caches/jiro/fields/hosts/. There is no jiro-specific cache
environment variable.
Refresh the current Principal's snapshot explicitly with:
jiro cache refreshAn expired snapshot is refreshed before use. If Jira cannot refresh it, jiro
continues with the stale mapping, including for Issue mutations, and emits a
warning. jiro field list --custom uses the same snapshot; the complete
field list remains live.
Descriptions and comments use Jira Markup by default. Request Jiro Flavored Markdown (JFM) conversion explicitly:
jiro issue add \
--project OPS \
--type Task \
--summary "Document rollout" \
--description-file issue.md \
--input-format=jfmmarkdown is a permanent, warning-free alias for the canonical jfm value.
The bidirectional format and its exact conversion behavior are specified in
docs/jiro-flavored-markdown.md.
Long text accepts inline input, a file path, or - for stdin. Inline and file
forms are mutually exclusive.
Convert documents locally without loading Jira configuration or credentials:
jiro jfm to-jira description.md
jiro jfm from-jira description.jira
cat description.md | jiro jfm to-jiraEach command accepts at most one file path. With no path or with -, it reads
stdin. Text mode writes only the exact converted document to stdout; conversion
warnings go to stderr. --output=json uses the normal versioned envelope with
jiraMarkup or jfm data and structured warnings.
jiro api sends one authenticated HTTP request to a relative endpoint of the
effective Jira Instance. It reuses the selected Profile, Credential,
authentication type, connection overrides, and configured context path such as
/jira. It does not call /rest/api/2/myself first or add a
/rest/api/2/ prefix.
jiro api rest/api/2/myself
jiro api 'rest/api/2/search?jql=project%20%3D%20OPS'
jiro api rest/api/2/issue -F 'fields={"project":{"key":"OPS"},"summary":"Example","issuetype":{"name":"Task"}}'
jiro api rest/api/2/issue/OPS-42 --method PATCH --input update.json
jiro api rest/api/2/issue/OPS-42/attachments \
--form file=@artifact.zip \
-H 'X-Atlassian-Token: no-check'The endpoint may start with / but must remain relative to the Jira Instance.
Absolute and scheme-relative URLs are rejected. Other URL syntax, including dot
segments, fragments, backslashes, controls, and encoding depth, is not
prevalidated. URL construction and Go's HTTP stack report any resulting error.
Supplying fields or a body changes the implicit method from GET to POST. An
explicit method is trimmed, uppercased, and passed to Go without an allowlist.
A read_only Profile permits only GET, HEAD, and OPTIONS.
--input and --form may still provide bodies for those explicit methods.
--input FILE|-streams bytes unchanged. With--input, fields are appended to the query string.-f, --string-field key=valuealways creates a JSON string.-F, --field key=valuedecodes a complete JSON value and otherwise uses a string.@fileand@-read the value from a file or stdin. Fields form a top-level JSON object and use command-line last-wins order for duplicate body keys. GET, HEAD, and OPTIONS place fields in the query string instead.--form key=valuecreates multipart form data.@fileand@-create file parts, while@@textsends the text@text.
Form mode is mutually exclusive with the other body modes.
Use repeatable -H, --header 'Name: value' flags for request headers.
Authorization, Proxy-Authorization, Host, Content-Length, and User-Agent are
managed by jiro. Content-Type is also managed in form mode.
Other header names and values are passed to Go's HTTP stack without
prevalidation. Repeated values for one name retain command-line order. An empty
value deletes all earlier values, and later values may add the header again.
Defaults are Accept: application/json and Content-Type: application/json
when a non-form body exists.
Go's standard compression behavior applies. When the Transport adds gzip, it
decompresses the response transparently. Supplying Accept-Encoding
explicitly leaves the response bytes to the caller.
Responses are Jira-owned bytes. Any 2xx body is streamed unchanged to stdout. A non-2xx body is streamed unchanged to stderr and returns the normal status-based exit code.
--include prepends the protocol, status, and sorted response headers visible
after Go's normal response processing. Set-Cookie values are not redacted.
Global --quiet suppresses only a successful body, and global --output is
unsupported for api.
The API command does not provide jq or template formatting, pagination,
caching, application-level retry, verbose tracing, or an output-file flag.
The default timeout is 30 seconds; --timeout 0 disables it. Redirects are
never followed, so every 3xx response is returned as a raw non-2xx API error.
API Requests use HTTP/1.1.
--insecure disables TLS certificate and hostname verification for the
current invocation. Reserve it for a trusted Jira Instance with a known
certificate problem. It is a silent no-op for HTTP Instances.
jiro uses Cobra's native completion support for Bash, Zsh, Fish, and PowerShell. Generate and load a script for the current session with:
# Bash
source <(jiro completion bash)
# Zsh (requires compinit)
source <(jiro completion zsh)
# Fish
jiro completion fish | source
# PowerShell
jiro completion powershell | Out-String | Invoke-ExpressionFor normal use, generate the script once in the shell's completion directory instead of running jiro during every shell startup:
mkdir -p ~/.zsh/completions
jiro completion zsh > ~/.zsh/completions/_jiro
# Add ~/.zsh/completions to fpath before running compinit in ~/.zshrc.
mkdir -p ~/.config/fish/completions
jiro completion fish > ~/.config/fish/completions/jiro.fishCompletion covers commands, flags, enum values, local input paths, and named Profiles from the selected config file. It does not contact Jira or read Credentials from the OS keyring.
make build
./bin/jiro --help
make fmt
make checkThe domain glossary is in CONTEXT.md. Durable design decisions
are recorded in docs/adr.
MIT