Skip to content

Output Formats

Every command accepts -o / --output to control the output format. The flag is registered globally, so read, write, delete, and every other subcommand honour it uniformly. Pick the one that matches what you’re doing: reading or scripting. For spreadsheets, see CSV export.

Default format, optimised for human reading.

Terminal window
redmine issues list --project myproject
ID TRACKER STATUS PRIORITY SUBJECT
123 Bug Open High Fix login timeout
124 Feature Open Normal Add dark mode
built-in jq

Every command that prints JSON accepts -q / --jq <expression> to filter its output with a jq expression. No external jq binary is needed.

Terminal window
# IDs of all open bugs, one per line
redmine issues list --tracker Bug --jq '.[].id'
# A single field of an issue
redmine issues get 123 --jq '.status.name'
# Capture the ID of a newly created issue
id=$(redmine issues create --project myproject --tracker Bug \
--subject "Crash on save" --jq '.id')
# Count issues by status
redmine issues list --project myproject \
--jq 'group_by(.status.name) | map({status: .[0].status.name, count: length})'
# Filter a raw API response
redmine api /users/current.json --jq '.user.login'

How it behaves:

  • --jq implies -o json. Combining it with an explicit -o table is an error.
  • Commands that never print JSON reject --jq: completion, mcp serve, mcp tools, attachments download --path - (raw file bytes) and the interactive commands (auth login, auth logout, auth switch without a profile name, issues browse, search browse).
  • The expression runs against exactly the JSON the command would print without --jq: resource bodies, lists (an empty list is []), action envelopes, and redmine api response bodies.
  • String results are printed raw, without quotes. Numbers, booleans, null, objects and arrays are printed as JSON. Each result goes on its own line. When stdout is a terminal, control characters in string results are neutralized (see Security hardening); piped output is written byte for byte.
  • Objects and arrays are compact (one per line) when stdout is piped, and indented when it is a terminal.
  • An empty or invalid expression fails with error code jq_invalid before any request is sent.
  • A runtime error (for example indexing an object as an array) exits non-zero with error code jq_failed and the message command succeeded, but --jq failed: .... The command itself did run: a create, update or delete has already happened, so do not retry it. Fix the expression, or fetch the resource again. The exception is redmine api -i on a failed (4xx/5xx) response: the message is then --jq failed on the error response: ... and the request itself failed.
  • halt and halt_error(0) stop the filter successfully, keeping any output produced before them; the halt_error(0) value is not printed. halt_error with any other exit code counts as a runtime error (jq_failed, exit code 1), so it can be used as a validation guard.
  • Output is held back until the expression finishes, so a runtime error normally leaves stdout empty. Once filtered output exceeds 1 MiB it is streamed instead, to keep memory bounded (e.g. --jq 'repeat(1)' | head); in that rare case a later runtime error can leave partial output on stdout.
  • Errors are never filtered: a failed command writes its error envelope to stderr as-is.
  • The expression can read the process environment through env and $ENV. With REDMINE_JQ_NO_ENV=1, both evaluate to {} instead. Set REDMINE_JQ_NO_ENV=1 in wrappers that pass jq expressions from an untrusted source, so they cannot read secrets such as REDMINE_API_KEY.

There is no csv output format. Build CSV from the JSON output with jq’s @csv instead, which also lets you pick the columns:

Terminal window
# ID and subject of every issue, one CSV row each
redmine issues list --project myproject -o json --jq '.[] | [.id, .subject] | @csv'
# With a header row, nested fields, and all pages
redmine issues list --project myproject --limit 0 -o json \
--jq '["ID", "Status", "Subject"], (.[] | [.id, .status.name, .subject]) | @csv'

@csv quotes strings and doubles embedded quotes; numbers are written as they are and null becomes an empty field. Use @tsv for tab-separated values. Written to a file or pipe, the values are not altered, so a cell that starts with =, +, - or @ may be evaluated as a formula when the file is opened in a spreadsheet app. Rows printed straight to a terminal have their control characters neutralized (see Security hardening).

-o csv fails before any request is sent, with a hint to use @csv. A profile with output_format: csv or REDMINE_OUTPUT_FORMAT=csv falls back to the table format and prints a warning; a command that picks its format with -o or --jq uses that format and prints no warning. The value csv is matched in any letter case.

-o json produces two top-level shapes on stdout:

  1. Raw resources for commands that fetch or return data (issues list, issues get, project create, …). The payload is the resource object (or array of objects) exactly as the API returned it.

    Collections follow one rule: if a collection was fetched, it is always an array, [] when empty. If it was not fetched, the key is absent. For example issues get 42 --journals always has journals, even with no history, so jq '.journals[]' is safe. Without --journals the key is missing, because an empty array would wrongly say the issue has no history. The same applies to --include sections (children, relations, attachments, trackers, memberships, …), and to custom_fields on issues, projects, and users, which are always fetched. One exception: Redmine leaves out collections the account may not see (for example watchers without the View watchers permission). Those stay absent rather than appearing as [], and issues watchers list fails with a permission hint instead of printing an empty list.

  2. Action envelope for mutators that have no natural response body (issues close, issues delete, issues assign, wiki delete, memberships delete, …). The envelope is stable and minimal:

    {
    "ok": true,
    "action": "deleted",
    "resource": "project",
    "id": "demo",
    "message": "Project \"demo\" deleted"
    }

    ok tells you whether the command changed state. Commands that complete successfully but have nothing to change may return the same envelope with ok: false and a descriptive message.

    The action vocabulary is small and intentional: created, updated, deleted, closed, reopened, assigned, unassigned, commented, logged, user_added, user_removed, logged_in, logged_out, switched, installed, opened, uploaded, archived, unarchived, watched, unwatched, related, unrelated.

When -o json (or --jq) is active and a command fails, the CLI writes a structured error envelope to stderr, leaves stdout empty, and exits non-zero. The envelope is a single line of compact JSON and always the last line of stderr, even when warnings or --verbose debug output come before it:

{"error":{"message":"Resource not found.","code":"not_found"}}

code comes from a small stable set: not_found, auth_failed, forbidden, validation_failed, conflict, server_error, jq_invalid, jq_failed, status_not_applied, timeout, already_exists, unknown. Validation and conflict errors include a details array with the individual messages from Redmine. validation_failed is also used, without details, when the CLI rejects an argument before sending anything, such as a project identifier or wiki page title that is not a single URL path segment (../users/2). jq_invalid means the --jq expression is empty or malformed and the command did not run; jq_failed means the --jq expression failed on the command’s output; the command itself succeeded unless the message says the output was an error response (see above). timeout means connecting to the server, or waiting for its response, timed out (see timeouts); already_exists means a download target already exists and --clobber was not given.

status_not_applied means Redmine accepted an issue update but silently kept the old status (see status changes are verified). Its details list the allowed statuses and, when the request carried other changes, a note that those may already have been applied.

Because the failure never reaches stdout, a pipeline such as redmine issues create ... -o json | jq '{id}' can no longer mistake an error for a result. Check the exit code (or set -o pipefail) to detect failures, and read the last line of stderr for the details:

Terminal window
if ! id=$(redmine issues create --project myproject --tracker Bug \
--subject "Crash on save" --jq '.id' 2>err.log); then
tail -n1 err.log | jq -r '.error.code + ": " + .error.message'
fi

Some errors also carry an optional hint with an actionable next step, typically a command to run. It is omitted when there is nothing useful to add; in non-JSON mode the same text is printed on a Hint: line.

{"error":{"message":"Validation error:\n - Category cannot be blank","code":"validation_failed","details":["Category cannot be blank"],"hint":"Required fields and allowed values depend on the project and tracker. Look them up with `redmine projects get myproject --include trackers,issue_categories,issue_custom_fields`, `redmine versions list --project myproject --open`, and `redmine memberships list --project myproject` (assignable users)."}}

Hints are currently emitted for:

  • forbidden: access comes from project roles (or admin status for admin-only endpoints), so retrying with other credentials won’t help. For a single issue, Redmine answers 403 when the issue exists but is private or in a project the user is not a member of.
  • not_found on a single issue: the issue does not exist on the server.
  • validation_failed on issue writes (issues create, update, assign, close, reopen, comment): commands that list valid values.

Redmine’s messages are localized; branch on code, not on message.

  • Machine-readable output (JSON, table rows) always goes to stdout.
  • Human-oriented messages (success confirmations in non-JSON mode, warnings, progress spinners, update-check hints) always go to stderr.
  • Errors always go to stderr, as Error: ... text or, in JSON mode, as the JSON error envelope. On failure stdout is empty.
  • One exception: redmine api prints the response of a 4xx or 5xx request to stdout (the body, and with -i the status and headers) and exits 1; with --silent it prints nothing and still exits 1. Only in JSON mode without -i or --silent is the failure reported as the error envelope on stderr instead (see api).

This split is stable — scripts can safely redirect stdout to a file or pipe it into jq without worrying about interleaved status noise.

Set a default in your config file:

~/.redmine-cli.yaml
output_format: table

The -o flag always overrides the default for a single command, and --jq always selects JSON.

Anyone who can edit an issue, wiki page or name on your Redmine controls part of what the CLI prints. The output layer treats that text as untrusted:

  • Terminal output. Tables, detail views, journals, status messages, errors, --verbose debug lines, the status line and headers printed by redmine api -i, and the interactive browsers (issues browse, search browse) neutralize terminal control characters in server data. Escape sequences are shown as visible text (ESC as ^[, BEL as ^G, a stray carriage return as ^M), C1 controls as \u009b-style escapes, and invisible bidi override characters are removed, so a crafted subject cannot set your clipboard, plant hyperlinks, move the cursor or overwrite lines. Regular Unicode (accents, CJK, emoji) is unaffected. JSON, redmine api bodies and attachment downloads are written byte for byte. So are --jq results when stdout is piped or redirected; on a terminal, string results (including @csv and @tsv rows) are neutralized the same way, while JSON results are not. Error messages from Redmine get the same treatment wherever they appear, including the details of JSON error envelopes and MCP tool errors.
  • --jq and the environment. env / $ENV work by default. REDMINE_JQ_NO_ENV=1 hides the environment from the expression.
  • Opening links. issues open and the open key in the interactive browsers only launch absolute http/https URLs, never strings starting with -, and never through a shell (Windows uses rundll32 url.dll,FileProtocolHandler). Issue search results are opened on your configured server. A link to any other host is shown in full in the status line and only opened when you press o a second time.