Terminal-pipeable, AI-discoverable Zendesk CLI. Single static binary, no runtime dependencies.
zd support tickets +list --status open --format table
zd support search 'type:ticket priority:urgent' --page-all | jq '.id'
zd aiaa bots +list
zd qa reviews +list --since 2026-01-01
zd docs ask "how does ticket merging work?"
zd api GET /api/v2/account.json
For AI agents: This section is written for you to execute, not for the user to read. If a user says something like "my boss sent me this link and told me to set up
zd", follow these steps top to bottom. You run every command in this guide — never instruct the user to run a command themselves. The only information you need to collect from the user is their Zendesk subdomain (the part before.zendesk.com). Everything else you can determine and do yourself. When a step involves the browser (OAuth consent), describe what the user will see in the browser and wait for them to confirm that the page loaded and they clicked Allow — that is the extent of user interaction.
uname -s # Darwin = macOS, Linux = Linux
uname -m # arm64 / aarch64 = Apple Silicon or ARM, x86_64 = Intel/AMD
echo $SHELL
Install via Scoop in PowerShell:
# Install Scoop if not already installed
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
irm get.scoop.sh | iex
# Add the shadowrock bucket (HTTPS — no SSH key required)
scoop bucket add shadowrock https://git.srk.rest/shadowrock/zencli.git
# Install zd
scoop install shadowrock/zd
Verify the install:
zd --version
Add PowerShell completion to the current user profile:
zd completion powershell | Out-File -Append $PROFILE
Windows limitations: The oauth-global and api-token auth methods work fully on Windows. The zendesk-session method decrypts v10 Chromium cookies on Windows using DPAPI (Chrome, Edge, Brave, Chromium). Chrome 127+ cookies using v20 App-Bound Encryption are not yet supported — the command returns an error with instructions to paste the cookie value manually from DevTools. All other commands work as expected. zd report-issue works for filing bugs; use it if you encounter unexpected behavior on Windows.
After install and completion setup, skip to Step 6 (keyring backend) below.
Check first:
which brew || echo "NOT_FOUND"
If Homebrew is missing on macOS:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
After install on Apple Silicon, add brew to PATH for the current session:
eval "$(/opt/homebrew/bin/brew shellenv)"
On Intel Mac, Homebrew is at /usr/local and is already on PATH after install.
If Homebrew is missing on Linux:
Install build dependencies first (Debian/Ubuntu):
sudo apt-get update && sudo apt-get install -y build-essential procps curl file git
On RHEL/CentOS/Fedora:
sudo yum groupinstall 'Development Tools' && sudo yum install -y procps-ng curl file git
Then install Homebrew:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
After install on Linux, add brew to PATH for the current session:
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
Add it permanently to the user's shell profile so it survives new sessions. Detect which profile file to use:
# If $SHELL is zsh
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.zprofile
# If $SHELL is bash
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.bash_profile
Verify brew works:
brew --version
brew tap shadowrock/tap ssh://git@git.srk.rest:2222/shadowrock/homebrew-tap.git
brew install shadowrock/tap/zd
If the SSH tap fails (no SSH key configured for git.srk.rest), try HTTPS:
brew tap shadowrock/tap https://git.srk.rest/shadowrock/homebrew-tap.git
brew install shadowrock/tap/zd
If both fail, the user needs network access to git.srk.rest. Tell them to contact the person who shared this link.
Verify the install:
zd --version
Expected output: zd 0.1.x+<sha> (<date>)
On macOS Apple Silicon, add to ~/.zprofile (or ~/.bash_profile for bash users):
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
On macOS Intel, this is not needed — /usr/local/bin is already on PATH.
Detect the user's shell and install completions:
# zsh (most macOS users)
if [ -n "$ZSH_VERSION" ] || [ "$SHELL" = "/bin/zsh" ] || [ "$SHELL" = "/usr/bin/zsh" ]; then
zd completion zsh > "${fpath[1]}/_zd" 2>/dev/null || \
zd completion zsh > /usr/local/share/zsh/site-functions/_zd 2>/dev/null || \
zd completion zsh > ~/.zfunc/_zd
fi
# bash
if [ -n "$BASH_VERSION" ] || [ "$SHELL" = "/bin/bash" ]; then
if [ -d /etc/bash_completion.d ]; then
zd completion bash | sudo tee /etc/bash_completion.d/zd > /dev/null
else
mkdir -p ~/.bash_completion.d
zd completion bash > ~/.bash_completion.d/zd
echo '[ -f ~/.bash_completion.d/zd ] && source ~/.bash_completion.d/zd' >> ~/.bashrc
fi
fi
On macOS with a desktop (GUI), no action is needed — Keychain handles credential storage.
In any headless context (SSH session, Linux without a desktop, CI, WSL2 without a GUI), add this to the shell profile to prevent the process from hanging on a missing GUI dialog:
# Check if running headless
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ] && [ "$(uname)" = "Linux" ]; then
echo 'export ZEN_KEYRING_BACKEND=file' >> ~/.zprofile
echo 'export ZEN_KEYRING_BACKEND=file' >> ~/.bash_profile
export ZEN_KEYRING_BACKEND=file
fi
On macOS in an SSH session without GUI forwarding, set it explicitly:
export ZEN_KEYRING_BACKEND=file
echo 'export ZEN_KEYRING_BACKEND=file' >> ~/.zprofile
Ask the user for one thing:
.zendesk.com in their Zendesk URL. If their URL is acme.zendesk.com, the subdomain is acme.That is all you need. OAuth handles the rest — no email address or API token required for setup.
Replace <subdomain> with the value from Step 7. Run this command:
zd auth login --subdomain <subdomain>
zd will open a browser tab automatically for the Zendesk OAuth consent screen. Inform the user: "A browser window has opened — please click Allow to grant access, then come back here." Once the user confirms they clicked Allow, the command completes and writes credentials to ~/.config/zd/credentials.toml.
After login completes, verify:
zd auth status
Expected: a JSON object showing the profile name, subdomain, auth method (oauth_global), and "valid": true.
Headless / no-browser fallback: If the environment has no browser (CI, remote SSH, etc.) and the user has a Zendesk API token, use the api-token method instead. First collect the token:
zd auth login --method api-token --subdomain <subdomain> --email <email> --token <token>
Confirm everything works end-to-end:
zd support tickets +list --limit 5 --format table
This fetches the 5 most recent tickets and prints them as a table. If the user's account has no tickets, the table will be empty — that is fine. A non-zero exit code or an error message means something is wrong with auth.
Try the docs command (no auth required):
zd docs ask "what is a Zendesk ticket?"
If the user will work across multiple Zendesk accounts later, they can name their profile explicitly. For a single account, the default profile name (default) is fine and no action is needed.
To rename it:
zd config set default_profile <subdomain>
| Symptom | Likely cause | Fix |
|---|---|---|
zd: command not found after install |
brew not on PATH | Run eval "$(brew shellenv)" and add it to shell profile |
brew tap fails with "Host key verification failed" |
No SSH key for git.srk.rest | Try the HTTPS tap URL or contact the person who shared this link |
zd auth status shows "valid": false |
Token entered incorrectly | Run zd auth login again |
| Command hangs with no output on macOS | Keychain GUI dialog waiting | Set ZEN_KEYRING_BACKEND=file (Step 6) and rerun |
error: Permission denied on completion install |
No write access to system dir | Write to user-local path instead (see Step 5) |
SSL certificate problem on Linux |
Missing CA certificates | Run sudo apt-get install -y ca-certificates or equivalent |
Error output contains "report_command" |
CLI bug (not a user error) | Run the report_command value verbatim — it files a GitLab issue automatically |
brew tap shadowrock/tap ssh://git@git.srk.rest:2222/shadowrock/homebrew-tap.git
brew install shadowrock/tap/zd
Once the tap is registered, upgrades use the short name:
brew upgrade zd
# zsh
zd completion zsh > "${fpath[1]}/_zd"
# bash
zd completion bash > /etc/bash_completion.d/zd
# fish
zd completion fish > ~/.config/fish/completions/zd.fish
Config lives in ~/.config/zd/. Two files matter:
| File | Purpose |
|---|---|
config.toml |
Profiles, default profile, output format, rate limit settings |
credentials.toml |
Encrypted tokens and cookies — do not edit by hand |
default_profile = "mycompany"
output_format = "json" # json | ndjson | table | yaml | csv
rate_limit_buffer = 0.8 # consume at most 80% of rate-limit headroom before pausing
keep_alive_interval = "5min" # QA/AIAA session renewal interval
# [partner] is optional — only needed to override the built-in OAuth client ID
# [partner]
# client_id = "your-custom-client-id"
[profile.mycompany]
name = "mycompany"
subdomain = "mycompany"
auth_method = "oauth_global"
[profile.mycompany-qa]
name = "mycompany-qa"
subdomain = "mycompany"
auth_method = "qa_shared_session"
Manage config values without editing the file directly:
zd config list
zd config get default_profile
zd config set default_profile mycompany
Every auth method writes credentials to credentials.toml and stores secrets in the OS keyring (macOS Keychain, Linux Secret Service). Credentials are encrypted at rest.
Suitable for headless and CI environments. Generate a token at Admin → Apps & Integrations → Zendesk API.
# Non-interactive (recommended for agents and scripts):
zd auth login --method api-token --subdomain mycompany --email you@example.com --token <token>
# Interactive (prompts for email and token if flags omitted):
zd auth login --method api-token --subdomain mycompany
Partner-level OAuth that works across all Zendesk accounts. No configuration required — the CLI uses a built-in client ID. Opens a browser for the consent flow.
zd auth login --method oauth-global --subdomain mycompany
# same as: zd auth login --subdomain mycompany
If you need to override the client ID (advanced/partner use), add a [partner] section to config.toml:
[partner]
client_id = "your-custom-client-id"
For endpoints that require a _zendesk_session browser cookie — ACM and some admin-only endpoints. Scans Chrome, Chromium, Edge, and Brave profiles automatically; falls back to a manual DevTools paste if decrypt fails or is unsupported (Windows, Chrome 127+ App-Bound Encryption).
zd auth login --method zendesk-session --subdomain mycompany
This method requires a desktop environment and is not agent-operable. Use oauth-global or api-token for automated contexts.
QA requires a browser session cookie. zd scans Chrome, Chromium, Edge, and Brave automatically on macOS and Linux:
zd auth chrome-decrypt-cookies --subdomain mycompany --target shared-session
Or import from a running browser session:
zd auth import-cookies --subdomain mycompany
Once imported, zd auth keep-alive renews the session every 5 minutes. Run it in a background terminal or tmux pane before a long QA session.
AIAA uses a 3-step JWT flow. The bridge handles the full exchange:
zd auth aiaa-bridge --profile mycompany-aiaa
The bridge token expires after 90 minutes. zd auth keep-alive handles renewal automatically when running.
zd auth status
zd auth status --profile mycompany-qa
macOS Keychain shows a GUI dialog when an unlocked keyring is needed. In headless contexts (CI, SSH sessions without forwarding, scripts), that dialog hangs indefinitely.
Set this env var to use file-backed encryption instead:
export ZEN_KEYRING_BACKEND=file
Add it to ~/.zprofile (or equivalent) to make it permanent. The encryption key is stored in ~/.config/zd/ rather than the system keyring.
Every command accepts -p / --profile to override the default:
zd -p mycompany-staging support tickets +list
zd -p mycompany-qa qa reviews +list --since 2026-01-01
To switch the default permanently:
zd config set default_profile mycompany-staging
JSON is the default. On a TTY, output is pretty-printed. When piped, it's compact. Override with --format:
zd support tickets +list --format table
zd support tickets +list --format csv > tickets.csv
zd support tickets +list --page-all --format ndjson | wc -l # one object per line, streaming
zd support tickets +list --format yaml
--page-all fetches all pages and emits each record as an NDJSON line, suitable for piping to jq, grep, or downstream tools.
+)Subcommands that operate on a named Zendesk resource (tickets, articles, bots, etc.) use a + prefix on the verb. This convention distinguishes resource verbs from top-level subcommands and keeps autocomplete unambiguous:
zd support tickets +list # list tickets
zd support tickets +get 12345 # get ticket 12345
zd support tickets +create ... # create a ticket
zd support tickets +update ... # update a ticket
zd support tickets +delete ... # delete a ticket
Common verbs: +list, +get, +create, +update, +delete. Not all resources support all verbs — check zd <product> <resource> --help.
zd support tickets +list --status open
zd support tickets +get 12345
zd support tickets +update 12345 --json '{"ticket":{"status":"solved"}}'
zd support users +list
zd support organizations +list
zd support groups +list
zd support search 'type:ticket assignee:me status:open'
zd support views +list
zd support macros +list
zd support tags +list --resource ticket --id 12345
zd support attachments upload ./file.pdf
zd support comments +list --ticket-id 12345
zd support job-statuses +get abc123
Auth methods: api_token, oauth_subdomain, oauth_global
zd hc articles +list --locale en-us
zd hc articles +get 360001234567
zd hc sections +list
zd hc categories +list
zd hc user-segments +list
zd hc permission-groups +list
Auth methods: api_token, oauth_subdomain, oauth_global
zd aiaa bots +list
zd aiaa conversations +list --bot-id abc123
zd aiaa procedures +list
zd aiaa training-data +list
zd aiaa data-export +list
zd aiaa sunshine messages +list --conversation-id xyz
Auth methods: aiaa_bridge, aiaa_export_key
Session note: AIAA bridge tokens have a 90-minute TTL. Run zd auth keep-alive to auto-renew during long operations. On BridgeJwtExpired errors, zd auth aiaa-bridge re-runs the full 3-step flow.
zd qa reviews +list --since 2026-01-01
zd qa scorecards +list
zd qa categories +list
zd qa connections +list
zd qa workspaces +list
zd qa filters +list
zd qa auto-qa +list
zd qa export +get-url
Auth methods: qa_shared_session, qa_export_bearer
Session note: Every authenticated QA response rotates the session cookie. zd captures and persists it automatically. Run zd auth keep-alive before long QA sessions to prevent expiry. All QA writes are serialized per profile to prevent cookie race conditions.
zd acm snapshots +list
zd acm snapshots +create --label baseline
zd acm environments +list
Queries Zendesk documentation — Help Center, developer docs, community, partner library, Postman collections, and more. Does not require a Zendesk profile.
# AI-generated answer with source citations
zd docs ask "how does ticket merging work?"
zd docs ask --stream "what is the rate limit for the tickets API?"
zd docs ask --show-sources "how do I use dynamic content?"
zd docs ask --source developer_docs "pagination cursor-based example"
# Raw ranked document chunks, no generation
zd docs search "oauth token endpoint"
zd docs search --source help_center --limit 5 "SLA policies"
Available content sources: help_center, developer_docs, garden_docs, smooch_docs, community_docs, partner_library, postman, dbt_zendesk, internal_note, marketplace_apps
Options:
--stream — stream response tokens as they arrive (ask only)--show-sources — print citations after the answer--source <prefix> — filter to a specific content source--limit <n> — number of chunks to retrieve (1–50, default 10)--retrieval-type hybrid|vector|keyword — retrieval method (default: hybrid)Hit any Zendesk API endpoint directly, authenticated with the active profile:
zd api GET /api/v2/account.json
zd api GET /api/v2/tickets.json
zd api POST /api/v2/tickets.json --json '{"ticket":{"subject":"Test","comment":{"body":"Hello"}}}'
zd api PATCH /api/v2/tickets/12345.json --json '{"ticket":{"status":"solved"}}'
zd api DELETE /api/v2/tickets/12345.json
zd api GET /api/v2/tickets.json --product support # explicit product context
File a bug report against the zd CLI itself. Use this when a CLI error message includes a "report_command" field — copy and run that command verbatim. Do not use this for Zendesk API errors, auth failures, or rate limits — only for CLI-level bugs.
# Run exactly as shown in the error output:
zd report-issue --error-code "PrivateEndpointBroken" --title "CLI bug: PrivateEndpointBroken" \
--command "zd support tickets +list"
# With a custom description:
zd report-issue --title "short title" --description "full markdown description"
When a reportable error occurs, zd automatically emits:
{
"code": "PrivateEndpointBroken { ... }",
"message": "undocumented endpoint /foo may have changed",
"hint": null,
"report_command": "zd report-issue --error-code \"PrivateEndpointBroken\" --title \"CLI bug: PrivateEndpointBroken\""
}
Run the report_command immediately. The command files an issue in shadowrock/zencli automatically — no GitLab account or credentials required.
All commands accept:
| Flag | Description |
|---|---|
-p, --profile <name> |
Override the default profile |
-f, --format <fmt> |
Output format: json, ndjson, table, yaml, csv |
--dry-run |
Print the request that would be sent without executing it |
-v, --verbose |
Emit trace-level logs including request/response headers |
--rate-limit-buffer <f> |
Fraction of rate-limit headroom to preserve (0.0–1.0) |
zd observes X-Rate-Limit-Remaining, ratelimit-reset, and Retry-After headers on every response. It backs off automatically before hitting the limit. The default buffer is 0.8 — it pauses when less than 20% of the bucket remains.
Adjust per invocation or in config.toml:
zd support tickets +list --page-all --rate-limit-buffer 0.5
cargo build --workspace
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --check
Integration tests use Wiremock and live in zd-core/tests/. Live end-to-end tests require a profile configured in ~/.config/zd/credentials.toml.
See PLAN.md for the implementation roadmap and CLAUDE.md for architecture notes and build conventions.