JSON Output
All commands support the global --json flag, with optional field selection
and a built-in --jq filter modeled on the gh CLI.
--json writes command data to stdout and command errors to stderr. Update
banners and retry notices are suppressed. HTTP debug traces from BB_DEBUG
also go to stderr, so stdout stays parseable with tracing on. Parser errors
such as unknown flags are JSON too (see Errors).
JSON is pretty-printed with two-space indentation when stdout is a terminal,
and compact (one line, no indentation) when stdout is a pipe or a file. The
data is the same either way; pipe to jq . if you want indentation in a
file. --jq results follow the same rule.
Quick usage
Section titled “Quick usage”# Full JSON outputbb pr list --json
# Project to a comma-separated field listbb pr list --json id,title,state
# Filter through jq (no external jq binary required)bb pr list --json id,title,state --jq '.[] | select(.state == "OPEN") | .title'
# Disable color formatting and return JSONbb repo view --json --no-colorField selection (--json fields)
Section titled “Field selection (--json fields)”Pass a comma-separated field list to project the output to just those fields,
matching the way gh CLI handles --json:
bb pr list --json id,title,statebb pr list --json id,title,author.display_nameRules:
-
Bare
--json(no field list) keeps the full output; backwards compatible. -
Top-level field names become the keys of each result object verbatim, including dotted paths (
author.display_namebecomes the literal key"author.display_name"). -
Dotted paths traverse nested objects; missing intermediates yield
null. -
If the output is already an array, each item is projected.
-
If the output is an envelope object, the CLI looks for the first of these keys holding an array and projects that array instead, dropping the envelope; the result is a flat array. The lookup is based on key names, not on the command or resource type.
pullRequests repositories snippets comments reviewers activitiesstatuses files pipelines commits workspaces projectsentries downloads results webhooks branchRestrictionssshKeys gpgKeys deployments environments values -
If no key in that list holds an array, the envelope object itself is projected.
Example: bb pr list --json id,title,author.display_name
Section titled “Example: bb pr list --json id,title,author.display_name”[ { "id": 42, "title": "Add feature", "author.display_name": "Alice" }, { "id": 41, "title": "Fix bug", "author.display_name": "Bob" }]Before / after: shape change after projection
Section titled “Before / after: shape change after projection”bb pr list --json returns the full envelope with metadata and a named array:
{ "workspace": "myworkspace", "repoSlug": "myrepo", "state": "OPEN", "filters": { "mine": false, "author": null, "reviewer": null, "source": null, "destination": null, "query": null }, "count": 2, "hasMore": false, "limit": 25, "pullRequests": [ { "id": 42, "title": "Add feature", "state": "OPEN", "author": { "display_name": "Alice", "nickname": "alice" }, "links": { "html": { "href": "..." } } }, { "id": 41, "title": "Fix bug", "state": "OPEN", "author": {...}, "links": {...} } ]}The same command with --json id,title,state drops the envelope and projects each item to a flat array of just those fields:
[ { "id": 42, "title": "Add feature", "state": "OPEN" }, { "id": 41, "title": "Fix bug", "state": "OPEN" }]This is why --jq filters use .[] after projection (.[] | select(...)) but .pullRequests[] against the full envelope.
Trap: envelopes whose array key is not in the list
Section titled “Trap: envelopes whose array key is not in the list”bb pipeline view returns { workspace, repoSlug, pipeline, steps }. steps
is not a wrapper key, so nothing is unwrapped and the field list is applied to
the envelope; where build_number does not exist:
bb pipeline view 7 --json build_number{ "build_number": null}Use --jq to reach into the envelope instead:
bb pipeline view 7 --json --jq '.pipeline.build_number'bb pipeline logs (without --step) puts its array under steps too. bb snippet view --files returns files as an object, not an array, so it is
not unwrapped either.
The reverse trap also exists. bb pr view returns a bare pull request, and that
payload carries a reviewers array (one of the recognized wrapper keys), so bb pr view 42 --json links.html.href projects the reviewers instead of the pull request. Use --jq
against the full output:
bb pr view 42 --json --jq '.links.html.href'bb api --paginate merges every page into {"values": [...]}, and values
is a wrapper key, so projection does unwrap it:
bb api /repositories/my-ws --paginate --json name,full_nameBuilt-in jq (--jq)
Section titled “Built-in jq (--jq)”The --jq <expression> flag pipes the JSON output through an embedded jq
engine (jq-wasm); no external jq
binary required, no bash pipe to fail on Windows.
# One value per line (strings come out quoted, see the raw-output note below)bb pr list --json --jq '.pullRequests[].title'
# Same, as plain text for the shellbb pr list --json --jq '.pullRequests[].title' --raw-output
# Combine with field selection (jq runs after projection)bb pr list --json id,title,state --jq '.[] | select(.state == "OPEN") | .title'
# Aggregatebb pr list --json --jq '.count'
# Repository namesbb repo list --json --jq '.repositories[].full_name'
# Diff file pathsbb pr diff 42 --stat --json --jq '.files[].path'
# Auth status as a bare booleanbb auth status --json --jq '.authenticated'
# Project + format with jq string interpolation as raw TSVbb pr list --json id,title,author.display_name --raw-output \ --jq '.[] | "\(.id)\t\(.["author.display_name"])\t\(.title)"'
# Pull request URLsbb pr list --json --jq '.pullRequests[].links.html.href'
# Project a nested URL out of a single resourcebb repo view --json links.html.href --jq '.["links.html.href"]'
# Top open PRs by author with built-in jq (no projection — full shape)bb pr list --json --jq '.pullRequests | group_by(.author.display_name) | map({ author: .[0].author.display_name, count: length }) | sort_by(-.count)'Every expression above also works with an external jq binary; pipe the full
output instead: bb pr list --json | jq '.pullRequests[].title'.
Rules:
-
By default
--jqbehaves likejqwithout-r, so string results keep their quotes:Terminal window bb pr list --json --jq '.pullRequests[].title'# "Add feature"# "Fix bug"Add
--raw-output(the built-in equivalent ofjq -r) when a script needs unquoted text. Objects, arrays, numbers and booleans are unaffected:Terminal window bb pr list --json --jq '.pullRequests[].title' --raw-output# Add feature# Fix bugWithout
--jq,--raw-outputexits non-zero with error 8002 (--raw-output requires --jq). -
--jqrequires--jsoneverywhere exceptbb api(next item). Elsewhere,--jqalone exits non-zero with error 8002 (--jq requires --json), reported as a JSON envelope on stderr because--jqasked for machine-readable output. -
bb apiis the exception: its output is already JSON, sobb api /repositories/my-ws --jq '.values[].name'works without--json. -
jq runs after field projection, so
.refers to the projected shape; a flat array when both--json fieldsand a wrapper-style command are involved. -
Invalid jq expressions exit non-zero with the underlying jq compiler error (error code 8001).
Lean output (--lean)
Section titled “Lean output (--lean)”Almost every Bitbucket object carries a links map of API, avatar and action
URLs, and in list output those maps are most of the bytes. --lean trims every
links map, at any depth, down to its html entry (the web URL) and drops
maps that have none. Everything else is untouched, so .links.html.href keeps
working:
bb pr list --json --leanbb pr list --json --lean --jq '.pullRequests[].links.html.href'{ "id": 42, "title": "Add feature", "author": { "display_name": "Jane Doe", "uuid": "{...}" }, "links": { "html": { "href": "https://bitbucket.org/ws/repo/pull-requests/42" } }}--lean is opt-in and needs --json (except in bb api). It runs after
field projection and before --jq, so --json links --lean also comes back
with only links.html. Clone URLs (links.clone) and other non-web links are
removed too; leave --lean off when you need them.
Output patterns
Section titled “Output patterns”Pattern 1: collection (list commands)
Section titled “Pattern 1: collection (list commands)”Used by: pr list, repo list, pr comments list, pr activity,
snippet list, snippet comments list, pipeline list, commit list,
status list, workspace list, project list, search code, webhook list
Returns an envelope with metadata, a count, paging info, and a named array:
{ "workspace": "myworkspace", "repoSlug": "myrepo", "state": "OPEN", "filters": { "mine": false }, "count": 2, "hasMore": false, "limit": 25, "pullRequests": [...]}hasMore is true when --limit cut the list short and Bitbucket has more
results; rerun with a higher --limit or --all to get them. limit is the
effective cap (25 by default) and null under --all. --json <fields>
drops the envelope, and these two keys with it, so check paging with --jq:
if [ "$(bb pr list --json --jq '.hasMore')" = "true" ]; then bb pr list --all --json > prs.jsonfiCommon envelope fields:
| Field | Type | Present in |
|---|---|---|
workspace |
string | All collections except workspace list (whose slugs live in the items) and the account-scoped ssh-key list / gpg-key list |
repoSlug |
string | Repository-scoped collections: pr list, pr comments list, pr activity, pipeline list, commit list, status list, repo ls, repo downloads list, webhook list (default --scope repo only), branch-restriction list, deployment list, deployment environments |
count |
number | All collections |
hasMore |
bool | All paginated collections (every command with --limit/--all) |
limit |
number | All paginated collections; null under --all |
state |
string | pr list |
filters |
object | pr list, pr activity, pr comments list, branch-restriction list; always present, with an unset value inside ({"mine": false}, {"types": []}, {"resolution": null}) or {} |
commit |
string | status list |
sort |
string | pipeline list |
ref, path |
string | repo ls |
query |
string | search code; the query as sent, including any repo: scope added by --repo |
Collection key names:
| Command | Array key |
|---|---|
pr list |
pullRequests |
repo list |
repositories |
pr comments list |
comments |
pr activity |
activities |
pipeline list |
pipelines |
commit list |
commits |
status list |
statuses |
workspace list |
workspaces |
project list |
projects |
repo ls |
entries |
repo downloads list |
downloads |
branch-restriction list |
branchRestrictions |
ssh-key list |
sshKeys |
gpg-key list |
gpgKeys |
deployment list |
deployments |
deployment environments |
environments |
snippet list |
snippets |
snippet comments list |
comments |
search code |
results |
webhook list |
webhooks |
Three commands return collections without going through the shared list
plumbing. They have no --limit/--all, and so carry no hasMore or
limit (pr checks always reads every page):
| Command | Shape |
|---|---|
pr reviewers list |
{ workspace, repoSlug, pullRequestId, count, reviewers } |
repo default-reviewers list |
{ workspace, repoSlug, mode, count, reviewers } |
pr checks |
{ pullRequestId, workspace, repoSlug, summary, statuses }; no count |
Example: bb pr list --json
Section titled “Example: bb pr list --json”{ "workspace": "myworkspace", "repoSlug": "myrepo", "state": "OPEN", "filters": { "mine": false }, "count": 2, "hasMore": false, "limit": 25, "pullRequests": [ { "id": 42, "title": "Add feature", "state": "OPEN", "draft": false, "author": { "display_name": "Alice", "nickname": "alice" }, "source": { "branch": { "name": "feature/add-feature" } }, "destination": { "branch": { "name": "main" } }, "created_on": "2026-01-15T10:00:00.000000+00:00", "updated_on": "2026-01-16T14:30:00.000000+00:00", "links": { "html": { "href": "https://bitbucket.org/myworkspace/myrepo/pull-requests/42" } } } ]}Example: bb pr list --mine --json
Section titled “Example: bb pr list --mine --json”{ "workspace": "myworkspace", "repoSlug": "myrepo", "state": "OPEN", "filters": { "mine": true, "author": null, "reviewer": "@me", "source": null, "destination": null, "query": null }, "count": 1, "hasMore": false, "limit": 25, "pullRequests": [...]}Example: bb pr checks 42 --json
Section titled “Example: bb pr checks 42 --json”{ "pullRequestId": 42, "workspace": "myworkspace", "repoSlug": "myrepo", "summary": { "successful": 2, "failed": 0, "pending": 1 }, "statuses": [...]}Pattern 2a: bare resource
Section titled “Pattern 2a: bare resource”Used by: pr view, pr create, pr edit, pr comments view, repo view,
repo create, snippet view (no --file/--files), snippet create,
snippet edit
Returns the raw Bitbucket API resource object directly, with no envelope:
{ "type": "pullrequest", "id": 42, "title": "Add feature", "state": "OPEN", "author": {...}, "source": {...}, "destination": {...}, "participants": [...], "links": {...}}Pattern 2b: enveloped resource
Section titled “Pattern 2b: enveloped resource”The rest of the single-resource commands wrap the payload in a context envelope. Read the resource from the named key, not from the root:
| Command | Shape |
|---|---|
commit view |
{ workspace, repoSlug, commit } |
project view, project create |
{ workspace, project } |
webhook view, webhook create |
{ workspace, repoSlug, webhook } (no repoSlug with --scope workspace) |
workspace view |
{ workspace } (the workspace object) |
status set |
{ workspace, repoSlug, commit, status } |
pr status |
{ workspace, repoSlug, currentBranch, currentBranchPullRequest, createdByYou, reviewRequested } |
pipeline view, pipeline watch |
{ workspace, repoSlug, pipeline, steps } |
pipeline run |
{ workspace, repoSlug, pipeline } |
pipeline logs --step <uuid> |
{ workspace, repoSlug, pipelineId, stepUuid, log } |
pipeline logs (no --step) |
{ workspace, repoSlug, pipelineId, count, steps } |
snippet view --file <name> |
{ file, content } |
snippet view --files |
{ snippet, files } (files is an object keyed by filename) |
pipeline view and pipeline logs put their array under steps, which is not
a projection wrapper key; see the trap above.
Pattern 3: action (approve/merge/decline commands)
Section titled “Pattern 3: action (approve/merge/decline commands)”Used by: pr approve, pr unapprove, pr request-changes, pr decline, pr merge, pr ready, pr comments add/edit/delete/reply/resolve/unresolve, pr reviewers add/remove, webhook delete, auth logout
Returns a success indicator plus resource identifiers:
{ "success": true, "pullRequestId": 42}Some action commands include the full resource in the response:
{ "success": true, "pullRequestId": 42, "pullRequest": {...}}webhook delete returns { "success": true, "workspace": "...", "repoSlug": "...", "webhookId": "{...}" }
(no repoSlug with --scope workspace).
Two action-style commands do not emit success; branch on their own key
instead:
auth login→{ "authenticated": true, "method": "oauth" | "api_token", "account": "...", "user": {...} }pipeline stop→{ "workspace": "...", "repoSlug": "...", "pipelineId": "...", "stopped": true }
(auth logout does emit success: { "authenticated": false, "success": true, "account": "..." },
plus "switchedTo" when another saved account became active and
"revokeFailed": true when the OAuth token could not be revoked.)
Dry run
Section titled “Dry run”Any command run with --dry-run prints
the write request it would have sent instead of its usual JSON, and exits 0:
{ "dryRun": true, "request": { "method": "POST", "url": "https://api.bitbucket.org/2.0/repositories/myworkspace/myrepo/pullrequests/42/merge", "body": { "type": "pullrequest_merge_parameters", "merge_strategy": "squash" } }}body is left out when the request has none.
Pattern 4: mode-based (diff command)
Section titled “Pattern 4: mode-based (diff command)”Used by: pr diff (with --stat, --web, --name-only, or default diff mode)
Returns context metadata plus mode-specific data. Every mode includes the same
context envelope (workspace, repoSlug, pullRequestId, mode); the
remaining fields depend on the mode.
| Mode | Trigger | Mode-specific fields |
|---|---|---|
diff |
(default) | diff (string; the raw unified diff text) |
stat |
--stat |
filesChanged, totalAdditions, totalDeletions, files[] |
name-only |
--name-only |
files[] (string array of paths) |
web |
--web |
url |
bb pr diff 42 --json (default mode)
Section titled “bb pr diff 42 --json (default mode)”The default mode returns the raw unified diff as a single string under diff.
Newlines are preserved, so the value is suitable for piping to git apply or
writing to a .patch file.
{ "workspace": "myworkspace", "repoSlug": "myrepo", "pullRequestId": 42, "mode": "diff", "diff": "diff --git a/src/index.ts b/src/index.ts\nindex abc123..def456 100644\n--- a/src/index.ts\n+++ b/src/index.ts\n@@ -1,3 +1,4 @@\n import { foo } from './foo';\n+import { bar } from './bar';\n \n export const main = () => {\n"}Extract just the diff text in scripts with --raw-output; without it,
--jq would emit the value as a quoted, escaped JSON string:
bb pr diff 42 --json --jq '.diff' --raw-output > pr-42.patchbb pr diff 42 --stat --json
Section titled “bb pr diff 42 --stat --json”{ "workspace": "myworkspace", "repoSlug": "myrepo", "pullRequestId": 42, "mode": "stat", "filesChanged": 3, "totalAdditions": 27, "totalDeletions": 11, "files": [{ "path": "src/index.ts", "additions": 10, "deletions": 3 }]}bb pr diff 42 --name-only --json
Section titled “bb pr diff 42 --name-only --json”{ "workspace": "myworkspace", "repoSlug": "myrepo", "pullRequestId": 42, "mode": "name-only", "files": ["src/index.ts", "src/utils.ts"]}bb pr diff 42 --web --json
Section titled “bb pr diff 42 --web --json”{ "workspace": "myworkspace", "repoSlug": "myrepo", "pullRequestId": 42, "mode": "web", "url": "https://bitbucket.org/myworkspace/myrepo/pull-requests/42/diff"}Pattern 5: auth & config (custom)
Section titled “Pattern 5: auth & config (custom)”These commands have unique output shapes:
bb auth status --json
Section titled “bb auth status --json”{ "authenticated": true, "account": "default", "accounts": ["default"], "method": "oauth", "user": { "username": "myuser", "displayName": "My Name", "accountId": "70121:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }, "defaultWorkspace": "myworkspace", "tokenExpiresAt": 1767225600}tokenExpiresAt (a Unix timestamp in seconds) is present only for OAuth
credentials. account is the account in use and accounts lists every saved
account. When the current account has no credentials the payload is:
{ "authenticated": false, "account": "default", "accounts": [] }The command also exits 1 in that case. Under set -e, append || true when
you want to read the payload instead of stopping.
method is "api_token" or "oauth", the same values auth login --json
reports. The config file stores API-token auth as authMethod: "basic", which
is what auth status --json reported in earlier releases. Check
authenticated first, and accept both values if your script must run against
older versions.
bb auth token --json
Section titled “bb auth token --json”OAuth credentials return the access token:
{ "token": "<access token>", "type": "bearer" }API-token credentials return the base64 of email:apiToken (the stored key is named username):
{ "token": "<base64 of email:apiToken>", "type": "basic" }bb config list --json
Section titled “bb config list --json”{ "configPath": "/Users/you/.config/bb/config.json", "config": { "defaultWorkspace": "myworkspace", "apiToken": "********", "skipVersionCheck": false }}bb config get <key> --json
Section titled “bb config get <key> --json”{ "key": "defaultWorkspace", "value": "myworkspace"}Scripting notes
Section titled “Scripting notes”- Prefer
--jsonin scripts rather than parsing text output. - Under
--json, stdout carries only the JSON document. Status lines such as “Opening browser…” go to stderr. - Do not rely on text formatting symbols (
✓, table separators, colors). - Prefer the built-in
--jqand--json fieldsover external pipes; they avoid an extra binary dependency and behave identically across platforms. - Envelope keys (
count,hasMore,limit,pullRequests,repositories,success) are owned by the CLI and stable. Item fields come straight from the Bitbucket API, so read.pullRequests[].idrather than assuming a shape for the whole item. pr listreturns lightweight PR objects. Usepr view <id>for full details includingparticipantsandreviewers.
Errors
Section titled “Errors”When a command fails, the CLI exits non-zero (see
exit codes, including the
opt-in BB_DETAILED_EXIT_CODES map).
- In normal mode, errors are plain text on stderr.
- In
--jsonmode, errors are compact JSON objects on stderr.--jqwithout--jsonalso reports its error this way.
Argument-parsing failures follow the same rule whenever --json or --jq
appears before any --: an unknown subcommand (bb pr lst --json), an unknown
option, a missing argument or too many arguments all come back as an envelope
with context.parseError, context.commandPath and a hint. An unknown
top-level command (bb prr --json) keeps its own shape,
context: { command, args }, naming the token that didn’t match. A group run
without a subcommand (bb pr --json) reports parseError: "missingSubcommand"
instead of printing its help.
{ "name": "BBError", "code": 5002, "message": "unknown option '--stat'", "context": { "parseError": "unknownOption", "commandPath": "pr list" }, "hint": "Run `bb pr list --help` for usage."}Example:
bb config get invalidKey --json 2>error.jsoncat error.json# {"name":"BBError","code":4003,"message":"Unknown config key 'invalidKey'. Valid keys: username, defaultWorkspace, credentialStorage, skipVersionCheck, versionCheckInterval, prCreateIncludeDefaultReviewers, gitProtocol","context":{"key":"invalidKey"}}Error JSON fields:
| Field | Type | Description |
|---|---|---|
name |
string | Error class name (usually BBError; can also be APIError, AuthError, GitError, ValidationError) |
code |
number | Numeric error code |
message |
string | Human-readable message |
context |
object | Optional structured metadata. Shape depends on code; see the table below |
hint |
string | Optional. A single actionable next step: present for 401, 403, and 404 failures where the CLI has advice to give, and for argument-parsing errors. In human-readable mode the API hints print as a dimmed continuation line beneath the message. |
statusCode |
number | APIError only. The upstream HTTP status. |
response |
object | string | APIError only, and only when the upstream response had a body. The raw Bitbucket error payload; usually a JSON object, but a plain-text body (e.g. a request-parser Bad Request) is passed through as a string. |
headers |
object | bb api only. The upstream response headers, so scripts can inspect rate-limit or request-id headers on failure. |
statusText |
string | bb api only, and only when the transport provides a reason phrase (HTTP/2 omits it). |
cause (the underlying Error) and stack traces are never exposed in --json
mode. Do not treat the field list above as closed: new optional keys such as
hint may be added, so parse defensively rather than asserting an exact key
set.
context fields by error code
Section titled “context fields by error code”The context object is omitted when there’s nothing useful to attach. When
present, the shape is:
| Code | Typical context keys |
|---|---|
1001 AUTH_REQUIRED |
(none) |
1002 AUTH_INVALID |
status (HTTP status, OAuth verification failures only) |
1003 AUTH_EXPIRED |
status (HTTP status from the token endpoint) |
2001–2005 API_* |
status, method, url; plus the top-level statusCode and response fields described above. url is the request path only; pagination and filter parameters the CLI sends are not part of it, though a query string you write yourself (bb api '/repositories/acme?role=owner') is |
3001 GIT_NOT_REPOSITORY |
(none; never thrown) |
3002 GIT_COMMAND_FAILED |
command, exitCode |
3003 GIT_REMOTE_NOT_FOUND |
remote |
4001 CONFIG_READ_FAILED |
(none) |
4002 CONFIG_WRITE_FAILED |
(none) |
4003 CONFIG_INVALID_KEY |
key |
5001 VALIDATION_REQUIRED |
field (when thrown by ValidationError); parseError, commandPath for a missing argument or option value |
5002 VALIDATION_INVALID |
varies; usually { key, value }, the offending option name, or the rejected ID. Argument-parsing errors carry parseError (unknownOption, unknownCommand, excessArguments, …) and commandPath |
5003 FILE_NOT_FOUND |
file (snippet create/edit/view), bodyFile (--body-file on pr create/edit, pr comments add/reply), or path (bb api -F key=@file / --input). snippet view --file also attaches available, the list of filenames in the snippet |
5005 CONFIRMATION_REQUIRED |
retry: the command to rerun with --yes, quoted for sh (PowerShell on Windows) |
6001 CONTEXT_REPO_NOT_FOUND |
reason (not_a_git_repo, no_remote, remote_not_bitbucket, ambiguous_remote, or unknown), optional remoteUrl or remotes (remote names) |
6002 CONTEXT_WORKSPACE_NOT_FOUND |
(none) |
7001 NETWORK_ERROR |
(usually none; OAuth revoke failures include status, body) |
8001 JQ_FAILED |
expression, optional exitCode |
8002 JSON_FORMAT_INVALID |
(none) |
9001 COMPLETION_INSTALL_FAILED |
(none) |
9002 COMPLETION_UNINSTALL_FAILED |
(none) |
10001 CI_FAILED |
pipeline, status (pipeline watch) or pullRequestId (pr checks --watch) |
9999 UNKNOWN |
(none) |
In automation, always check the process exit code before parsing stdout JSON.