Skip to content

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.

Terminal window
# Full JSON output
bb pr list --json
# Project to a comma-separated field list
bb 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 JSON
bb repo view --json --no-color

Pass a comma-separated field list to project the output to just those fields, matching the way gh CLI handles --json:

Terminal window
bb pr list --json id,title,state
bb pr list --json id,title,author.display_name

Rules:

  • 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_name becomes 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 activities
    statuses files pipelines commits workspaces projects
    entries downloads results webhooks branchRestrictions
    sshKeys 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:

Terminal window
bb pipeline view 7 --json build_number
{
"build_number": null
}

Use --jq to reach into the envelope instead:

Terminal window
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:

Terminal window
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:

Terminal window
bb api /repositories/my-ws --paginate --json name,full_name

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.

Terminal window
# 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 shell
bb 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'
# Aggregate
bb pr list --json --jq '.count'
# Repository names
bb repo list --json --jq '.repositories[].full_name'
# Diff file paths
bb pr diff 42 --stat --json --jq '.files[].path'
# Auth status as a bare boolean
bb auth status --json --jq '.authenticated'
# Project + format with jq string interpolation as raw TSV
bb pr list --json id,title,author.display_name --raw-output \
--jq '.[] | "\(.id)\t\(.["author.display_name"])\t\(.title)"'
# Pull request URLs
bb pr list --json --jq '.pullRequests[].links.html.href'
# Project a nested URL out of a single resource
bb 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 --jq behaves like jq without -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 of jq -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 bug

    Without --jq, --raw-output exits non-zero with error 8002 (--raw-output requires --jq).

  • --jq requires --json everywhere except bb api (next item). Elsewhere, --jq alone exits non-zero with error 8002 (--jq requires --json), reported as a JSON envelope on stderr because --jq asked for machine-readable output.

  • bb api is the exception: its output is already JSON, so bb 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 fields and a wrapper-style command are involved.

  • Invalid jq expressions exit non-zero with the underlying jq compiler error (error code 8001).

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:

Terminal window
bb pr list --json --lean
bb 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.

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:

Terminal window
if [ "$(bb pr list --json --jq '.hasMore')" = "true" ]; then
bb pr list --all --json > prs.json
fi

Common 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
{
"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"
}
}
}
]
}
{
"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": [...]
}
{
"pullRequestId": 42,
"workspace": "myworkspace",
"repoSlug": "myrepo",
"summary": { "successful": 2, "failed": 0, "pending": 1 },
"statuses": [...]
}

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": {...}
}

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.)


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.


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

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:

Terminal window
bb pr diff 42 --json --jq '.diff' --raw-output > pr-42.patch
{
"workspace": "myworkspace",
"repoSlug": "myrepo",
"pullRequestId": 42,
"mode": "stat",
"filesChanged": 3,
"totalAdditions": 27,
"totalDeletions": 11,
"files": [{ "path": "src/index.ts", "additions": 10, "deletions": 3 }]
}
{
"workspace": "myworkspace",
"repoSlug": "myrepo",
"pullRequestId": 42,
"mode": "name-only",
"files": ["src/index.ts", "src/utils.ts"]
}
{
"workspace": "myworkspace",
"repoSlug": "myrepo",
"pullRequestId": 42,
"mode": "web",
"url": "https://bitbucket.org/myworkspace/myrepo/pull-requests/42/diff"
}

These commands have unique output shapes:

{
"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.

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" }
{
"configPath": "/Users/you/.config/bb/config.json",
"config": {
"username": "[email protected]",
"defaultWorkspace": "myworkspace",
"apiToken": "********",
"skipVersionCheck": false
}
}
{
"key": "defaultWorkspace",
"value": "myworkspace"
}

  • Prefer --json in 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 --jq and --json fields over 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[].id rather than assuming a shape for the whole item.
  • pr list returns lightweight PR objects. Use pr view <id> for full details including participants and reviewers.

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 --json mode, errors are compact JSON objects on stderr. --jq without --json also 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:

Terminal window
bb config get invalidKey --json 2>error.json
cat 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.

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.