ado — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ado (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use az CLI commands for most operations and bash scripts for complex multi-step orchestration.
Credentials come from environment variables exported in the terminal before the session starts; they are inherited read-only. This skill only reads them - it never exports or mutates session credentials. The expected shape is the plain triple ADO_ORG, ADO_PROJECT, AZURE_DEVOPS_EXT_PAT - commands use these directly.
When nothing is exported, two more sources can stand in: a sequester secrets profile read per command block (see "Sequester profiles" below), or an az login session (see "az CLI token fallback" below).
Check what is available:
echo "ORG=${ADO_ORG:-MISSING} PROJECT=${ADO_PROJECT:-MISSING} PAT=${AZURE_DEVOPS_EXT_PAT:+set} TOKEN=${ADO_TOKEN:+set}"
command -v sequester >/dev/null && sequester secret list # sequester profiles (names only, no prompt)
az account show --query '[user.name, tenantId]' -o tsv 2>/dev/null || echo "az: not logged in"
# Entra tenant behind the org (anonymous request, no credentials needed)
curl -sS -o /dev/null -D - "$ADO_ORG/_apis/connectionData" | tr -d '\r' \
| awk -F': *' 'tolower($1) == "x-vss-resourcetenant" { print "org tenant: " $2 }'When the two tenants differ, plain az devops commands fail under the az login fallback, so run them through ado-exec.sh (see "az CLI token fallback" below).
When no credential env vars are inherited and the sequester CLI is on PATH, read the PAT from a sequester secrets profile per command block. The convention is one profile per org, named as the org URL segment (the <org> in https://dev.azure.com/<org>), holding a single AZURE_DEVOPS_EXT_PAT - PATs are org-scoped, so the org identifies both the profile and the token. sequester secret list prints profile names and their variable names without prompting.
ADO_ORG and ADO_PROJECT are not secrets. Derive them from the clone path (.../dev.azure.com/<org>/<project>/...) or the repo remote, and set them inside the wrapped block - the profile supplies only the PAT:
sequester env exec <org> -- bash -c '
export ADO_ORG=https://dev.azure.com/<org> ADO_PROJECT=<project>
az repos pr list -r <repo> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
'Single-quote the block so the PAT never touches the calling shell. Each env exec is one profile read and may require a Touch ID tap from the user, so batch every command that needs credentials into one block per exec rather than wrapping commands individually. The bundled scripts read the same variables and run the same way, with the export line before the script call.
The az commands in that block can still run as a signed-in az login account instead of the PAT, because the az devops extension tries those accounts first. To rule that out, run the block through ado-exec.sh inside the exec: sequester env exec <org> -- ${CLAUDE_SKILL_DIR}/scripts/ado-exec.sh --org https://dev.azure.com/<org> -- bash -c '...' (see "az CLI token fallback" below).
Multi-org falls out of the naming: each org is its own profile, one block binds one org, and a cross-org task (for example a migration) reads in one exec and writes in another. A profile holding the full ADO_ORG / ADO_PROJECT / AZURE_DEVOPS_EXT_PAT triple also works - the exec injects all three and the export line is unnecessary.
Once org and project are known, auth resolves in this order (mirrors scripts/lib/ado-client.sh, which the bundled scripts use automatically):
az CLI picks it up automatically; curl uses Basic auth.az login. The token is minted for the Entra tenant that backs the org, which can differ from the tenant of your Azure subscriptions.Under the az login fallback, run each command block through ado-exec.sh. It mints the token and exports it to the wrapped command only, as ADO_TOKEN, as AZURE_DEVOPS_EXT_PAT for the az devops, az repos, az pipelines, az boards and az artifacts commands, and as ADO_AUTH_HEADER for curl. With a PAT exported, the wrapper uses the PAT as it is. The wrapper writes no credential to disk, and the calling shell never sees the token.
The az devops extension tries cached az login accounts before the exported credential, so with several accounts signed in to az, an az command could run as a different identity than curl. The wrapper prevents this by running the command with AZURE_CONFIG_DIR set to an empty temporary directory, which it removes afterwards. The az commands in the block then find no az login account and run as the exported credential. Azure resource commands such as az account need your login, so pass --shared-az-config for a block that runs them. The az devops commands in that block can then pick another signed-in account again (see reference.md). Batch the commands into one block per run, as with a sequester exec:
${CLAUDE_SKILL_DIR}/scripts/ado-exec.sh -- bash -c '
az boards work-item show --id <ID> --org "$ADO_ORG" --detect false
curl -sS -H "$ADO_AUTH_HEADER" "$ADO_ORG/_apis/connectionData"
'The wrapper exists because the az devops extension asks az login for tokens only in the tenants of your cached subscriptions. When the org sits in another tenant, every az devops call fails with "has not been materialized" (see Troubleshooting). The wrapper reads the org's tenant from the X-VSS-ResourceTenant header of an anonymous request to $ADO_ORG/_apis/connectionData and runs az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --tenant <org tenant>. The GUID is the public Azure DevOps resource, the same in every tenant. If your account is a member or guest of the org's tenant, this works from your existing az login without a prompt. Set ADO_TENANT_ID to skip the lookup. The bundled scripts do the same on their own.
Minted tokens expire after about an hour, and each wrapper run mints a new one. When Azure DevOps rejects a minted token, the bundled scripts mint once more and retry the request once. They never replace a PAT or ADO_TOKEN that you supplied. After an unexpected 401, a 302 to sign-in or an HTML page in an ad-hoc block, check which commands in the block already went through. Rerun only the failed command through the wrapper before you suspect a permissions problem, and never replay a write that may have been applied. The Bearer token also works against the vssps.dev.azure.com and feeds.dev.azure.com hosts.
Keep az login --tenant <org tenant> --allow-no-subscriptions for orgs whose tenant demands an interactive sign-in, because that login can switch your default subscription to a tenant-level account in the org's tenant. reference.md ("Authentication internals") has the details and the error-by-error fixes.
If ADO_ORG or ADO_PROJECT is missing, derive them from the repo remote when possible (git remote get-url origin; HTTPS remotes look like https://dev.azure.com/<org>/<project>/_git/<repo>, SSH like [email protected]:v3/<org>/<project>/<repo>) and set them for the commands you run.
Set AUTH once from whichever source is available - the curl snippets in this skill pass -H "$AUTH" and work with either form. Under a sequester profile or ado-exec.sh, build AUTH inside the wrapped block - the variables only exist there:
if [[ -n "${ADO_AUTH_HEADER:-}" ]]; then
AUTH="$ADO_AUTH_HEADER" # set by ado-exec.sh
elif [[ -n "${AZURE_DEVOPS_EXT_PAT:-}" ]]; then
AUTH="Authorization: Basic $(printf ':%s' "$AZURE_DEVOPS_EXT_PAT" | base64 | tr -d '\n')"
else
AUTH="Authorization: Bearer ${ADO_TOKEN:?no credentials - run this block through ado-exec.sh}"
fiStop and instruct the user only when no auth source works (no PAT in either shape, no sequester profile holding the triple, no ADO_TOKEN, and az account show fails):
Either runaz loginin your terminal, create a sequester profile named after your org (sequester secret set <org> AZURE_DEVOPS_EXT_PAT), or set the following (outside Claude Code) and start a new session:
>
`` export ADO_ORG=https://dev.azure.com/<org> export ADO_PROJECT=<project> export AZURE_DEVOPS_EXT_PAT=<pat> ``Prefer the az CLI; fall back to curl -H "$AUTH" for REST endpoints that az doesn't cover (az rest is ARM-only and does not work against ADO APIs).
All az commands use --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false.
Exception: These subcommands are org-scoped - they take --org but not --project:
az boards work-item show, update, relation addaz repos pr show, update, set-voteaz repos pr work-item addaz repos pr reviewer addaz repos pr policy listaz repos pr policy queueOnly az boards work-item create and az boards query accept --project.
# List PRs
az repos pr list -r <repo> --status active --top N --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
# Show PR details (org-scoped - no --project)
az repos pr show --id <ID> --org "$ADO_ORG" --detect false
# Show PR policy evaluations
az repos pr policy list --id <ID> --org "$ADO_ORG" --detect false
# Create PR
az repos pr create -r <repo> -s <branch> --title "text" \
--auto-complete true --squash true --delete-source-branch true \
--required-reviewers [email protected] [email protected] \
--org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
# Update PR
az repos pr update --id <ID> --title "new" --description "new" \
--auto-complete true --squash true \
--org "$ADO_ORG" --detect false
# Complete/merge PR
az repos pr update --id <ID> --status completed --squash true \
--delete-source-branch true --bypass-policy true \
--org "$ADO_ORG" --detect false
# Abandon PR
az repos pr update --id <ID> --status abandoned --org "$ADO_ORG" --detect false
# Vote on PR
az repos pr set-vote --id <ID> --vote approve --org "$ADO_ORG" --detect false
# Add reviewers (optional or required)
# NOTE: az repos pr reviewer add fails under PAT auth with "A valid reviewer must
# be supplied." regardless of input format (email, entitlement GUID, identity GUID).
# Use REST PUT with an identity GUID resolved from vssps - see "Reviewer identity
# resolution" below.
az repos pr reviewer add --id <ID> --reviewers <GUID1> <GUID2> --required true \
--org "$ADO_ORG" --detect false
# Link work item to PR
az repos pr work-item add --id <PR_ID> --work-items <WI_ID> \
--org "$ADO_ORG" --detect false
# Requeue failed BVP
az repos pr policy list --id <ID> -o json --org "$ADO_ORG" --detect false # find failed evaluation IDs
az repos pr policy queue --id <ID> -e <eval-id> --org "$ADO_ORG" --detect falseAdding PR reviewers under PAT auth requires the vssps identity id, not the entitlement id returned by az devops user list. They are different GUIDs for the same user, and only the vssps one works. az repos pr reviewer add fails for both, so use REST:
# AUTH as set in the Authentication section (Basic PAT or az Bearer token)
VSSPS="${ADO_ORG/dev.azure.com/vssps.dev.azure.com}"
REPO_ID=$(az repos show -r <repo> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false --query id -o tsv)
# 1. Resolve identity id from email
REVIEWER_ID=$(curl -sS -H "$AUTH" \
"${VSSPS}/_apis/identities?searchFilter=General&filterValue=<email>&api-version=7.1" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['value'][0]['id'])")
# 2. Add as required reviewer
curl -sS -X PUT -H "$AUTH" -H "Content-Type: application/json" \
"${ADO_ORG}/_apis/git/repositories/${REPO_ID}/pullRequests/<PR_ID>/reviewers/${REVIEWER_ID}?api-version=7.1" \
-d "{\"vote\":0,\"isRequired\":true,\"id\":\"${REVIEWER_ID}\"}"If the PUT response body comes back empty (no displayName, no error), the id was wrong - re-resolve via vssps instead of retrying with the same value. az devops user list ids silently fail this way.
All PR comment operations require curl with $AUTH (az rest does not work with ADO APIs).
# AUTH as set in the Authentication section (Basic PAT or az Bearer token)
THREADS="${ADO_ORG}/${ADO_PROJECT}/_apis/git/repositories/<repo>/pullRequests/<prId>/threads"
# List active threads (script - handles filtering + formatting)
${CLAUDE_SKILL_DIR}/scripts/ado-pr-threads.sh --pr-id <ID> --repo <repo> --status active
# List threads on a specific file
${CLAUDE_SKILL_DIR}/scripts/ado-pr-threads.sh --pr-id <ID> --repo <repo> --file "/src/main.tf"
# Create general comment
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST \
"${THREADS}?api-version=7.1" \
-d '{"comments":[{"content":"LGTM, one question about error handling.","commentType":1}],"status":"active"}'
# Create file-level comment
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST \
"${THREADS}?api-version=7.1" \
-d '{"comments":[{"content":"Should use a data source here.","commentType":1}],"status":"active","threadContext":{"filePath":"/src/main.tf","rightFileStart":{"line":42,"offset":1},"rightFileEnd":{"line":42,"offset":1}}}'
# Code suggestion (uses ```suggestion block - ADO renders as applicable diff)
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST \
"${THREADS}?api-version=7.1" \
-d '{"comments":[{"content":"Consider:\n\n```suggestion\ndata \"azurerm_resource_group\" \"example\" {\n name = var.rg_name\n}\n```","commentType":1}],"status":"active","threadContext":{"filePath":"/src/main.tf","rightFileStart":{"line":42,"offset":1},"rightFileEnd":{"line":44,"offset":1}}}'
# Reply to thread
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST \
"${THREADS}/<threadId>/comments?api-version=7.1" \
-d '{"parentCommentId":1,"content":"Fixed in latest push.","commentType":1}'
# Update a comment
curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH \
"${THREADS}/<threadId>/comments/<commentId>?api-version=7.1" \
-d '{"content":"Updated response."}'
# Delete a comment (soft-delete, returns HTTP 200)
curl -s -H "$AUTH" -H "Content-Type: application/json" -X DELETE \
"${THREADS}/<threadId>/comments/<commentId>?api-version=7.1"
# Resolve thread
curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH \
"${THREADS}/<threadId>?api-version=7.1" \
-d '{"status":"fixed"}'
# Status values: active, fixed, wontFix, closed, byDesign, pending
# Reactivate thread
curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH \
"${THREADS}/<threadId>?api-version=7.1" \
-d '{"status":"active"}'az pipelines list --name <filter> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines runs list --pipeline-ids <id> --top N --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines run --id <id> --branch <branch> --parameters key=value --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines build cancel --build-id <id> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines create --name <name> --repository <repo> --yml-path <path> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines update --id <id> --name <name> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines delete --id <id> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect falseaz pipelines run does not expose stagesToSkip. Use the /pipelines/{id}/runs REST endpoint directly. Stages listed in stagesToSkip end skipped, and downstream stages that depend on them run as if the upstream succeeded - so this is the correct way to rerun e.g. only a cleanup/destroy stage after a canceled apply, without editing the pipeline YAML or adding approval gates.
# Identifier is the YAML `- stage: <id>`, not the displayName. Grab it from a
# prior run's timeline: `records[].type == 'Stage'` -> `.identifier`.
curl -sS -H "$AUTH" -X POST \
"$ADO_ORG/$ADO_PROJECT/_apis/pipelines/<pipelineId>/runs?api-version=7.1-preview.1" \
-H "Content-Type: application/json" \
-d '{
"resources": { "repositories": { "self": { "refName": "refs/heads/<branch>" } } },
"stagesToSkip": ["RepoValidate", "TerraformValidate", "TerraformPlan", "TerraformApply"]
}'Common use case: previous BVP run was canceled mid-apply, leaving an orphan resource group in state. Skip every upstream stage and let only TerraformDestroy run - it reads the existing state backend and tears down what the canceled apply created.
Contrast with cancellation cascade (which is _not_ overridden by stagesToSkip on a running build): when a running build is canceled, the upstream ends Canceled and downstream defaults to Skipped via the implicit succeeded() condition. No rerun button appears on the skipped stage. The fix there is a fresh queue with stagesToSkip as above, not re-running within the canceled build.
Note: the older /build/builds?api-version=7.1 endpoint silently ignores stagesToSkip. Use /pipelines/{id}/runs?api-version=7.1-preview.1.
# Step 1: Get repository ID
REPO_ID=$(az repos show -r <repo> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false --query id -o tsv)
# Step 2: List policies filtered by repository
az repos policy list --repository-id "$REPO_ID" --branch <branch> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az repos policy update --id <id> --blocking true --enabled true --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az repos policy delete --id <id> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect falseCreate policy (complex - use script for identity resolution):
${CLAUDE_SKILL_DIR}/scripts/ado-create-policy.sh --repo <name> --type build --pipeline-id <id> [--branch main] [--display-name TEXT] [--blocking]
${CLAUDE_SKILL_DIR}/scripts/ado-create-policy.sh --repo <name> --type approver [--min-approvers N] [--creator-vote] [--reset-on-push] [--branch main] [--blocking]
${CLAUDE_SKILL_DIR}/scripts/ado-create-policy.sh --repo <name> --type required-reviewer --reviewer <email|name|team|GUID> [--reviewer ...] [--filename-patterns "/*.tf,/pipelines/*"] [--branch main] [--blocking]az pipelines variable-group list --group-name <filter> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines variable-group show --id <id> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines variable-group variable create --group-id <id> --name KEY --value VALUE --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines variable-group variable update --group-id <id> --name KEY --new-value VALUE --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines variable-group variable delete --group-id <id> --name KEY --org "$ADO_ORG" -p "$ADO_PROJECT" --detect falseProject-specific custom fields and work item types should be defined in your CLAUDE.md. See examples/project-claude-ado.md for a template.
# Show work item (all fields + relations)
az boards work-item show --id <ID> --expand all --org "$ADO_ORG" --detect false -o json
# Show specific fields only (-f needs --expand none, see Troubleshooting)
az boards work-item show --id <ID> -f "System.Title,System.State,System.AssignedTo,<custom-field-1>,<custom-field-2>" --expand none --org "$ADO_ORG" --detect false -o json
# Query work items (WIQL) - this command accepts --project
az boards query --wiql "SELECT [System.Id], [System.Title], [System.State] FROM workitems WHERE [System.AreaPath] UNDER '<AreaPath>' AND [System.State] = 'Active' AND [System.WorkItemType] = '<TaskType>'" --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
# List child work items of a parent
az boards query --wiql "SELECT [System.Id], [System.Title], [System.State], [System.AssignedTo] FROM workitems WHERE [System.Parent] = <PARENT_ID>" --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
# Show work item with relations (to find parent/child links)
az boards work-item show --id <ID> --expand relations --org "$ADO_ORG" --detect false -o json
# Parse parent: jq '.relations[] | select(.attributes.name == "Parent") | .url' - extract ID from URL tail# Create task under a project
az boards work-item create --type "<TaskType>" --title "Implement feature X" \
--description "What needs to be done and why" \
--assigned-to "[email protected]" \
--area "<AreaPath>" \
--iteration "<IterationPath>" \
-f "<custom-field-1>[email protected]" "<custom-field-2>[email protected]" \
--org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
# Update work item (state, assignment, custom fields)
az boards work-item update --id <ID> --state Active --assigned-to "[email protected]" \
--org "$ADO_ORG" --detect false
# Set custom fields on existing work item
az boards work-item update --id <ID> \
-f "<custom-field-1>[email protected]" "<custom-field-2>[email protected]" \
--org "$ADO_ORG" --detect false
# Quick append discussion comment (az CLI shorthand - no comment ID returned, no @mention support)
az boards work-item update --id <ID> --discussion "Started work on this" \
--org "$ADO_ORG" --detect falseFull CRUD on work item comments requires REST API (curl with $AUTH). The --discussion flag above is append-only and does not support @mentions.
@Mentions in comments: Use the REST API with HTML mention format. The --discussion flag renders mentions as plain text.
# AUTH as set in the Authentication section (Basic PAT or az Bearer token)
WI_COMMENTS="${ADO_ORG}/${ADO_PROJECT}/_apis/wit/workItems/<id>/comments"
# Post comment with @mention (use identity GUID and email)
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST \
"${WI_COMMENTS}?api-version=7.1-preview.4" \
-d '{"text":"<a href=\"mailto:[email protected]\" data-vss-mention=\"version:2.0,<IDENTITY_GUID>\">@Display Name</a> please review."}'# AUTH as set in the Authentication section (Basic PAT or az Bearer token)
WI_COMMENTS="${ADO_ORG}/${ADO_PROJECT}/_apis/wit/workItems/<id>/comments"
# List comments (most recent first)
curl -s -H "$AUTH" -H "Content-Type: application/json" \
"${WI_COMMENTS}?\$top=10&order=desc&api-version=7.1-preview.4"
# Add comment (returns comment with id for later update/delete)
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST \
"${WI_COMMENTS}?api-version=7.0-preview.3" \
-d '{"text":"Started investigation. Root cause is missing RBAC assignment."}'
# Update comment
curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH \
"${WI_COMMENTS}/<commentId>?api-version=7.1-preview.4" \
-d '{"text":"Updated: fix is in PR #142."}'
# Delete comment (soft-delete, returns HTTP 204)
curl -s -H "$AUTH" -H "Content-Type: application/json" -X DELETE \
"${WI_COMMENTS}/<commentId>?api-version=7.1-preview.4"# Add parent relation (make task child of user story)
az boards work-item relation add --id <CHILD_ID> --relation-type parent --target-id <PARENT_ID> \
--org "$ADO_ORG" --detect false
# Add child relation
az boards work-item relation add --id <PARENT_ID> --relation-type child --target-id <CHILD_ID> \
--org "$ADO_ORG" --detect false
# Link work item to PR (from PR side - preferred)
az repos pr work-item add --id <PR_ID> --work-items <WI_ID> \
--org "$ADO_ORG" --detect false
# List relation types available
az boards work-item relation list-type --org "$ADO_ORG" --detect false# List teams in project
az devops team list --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
# List team members
az devops team list-member --team "TeamName" --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false# PR threads (list, filter by status/file)
${CLAUDE_SKILL_DIR}/scripts/ado-pr-threads.sh --pr-id <id> --repo <repo> --status active
${CLAUDE_SKILL_DIR}/scripts/ado-pr-threads.sh --pr-id <id> --repo <repo> --file "/src/main.tf" --json
# Run details with stage/job tree
${CLAUDE_SKILL_DIR}/scripts/ado-get-run.sh --run-id <id>
# Task logs
${CLAUDE_SKILL_DIR}/scripts/ado-get-logs.sh --run-id <id> --failed-only
${CLAUDE_SKILL_DIR}/scripts/ado-get-logs.sh --run-id <id> --task "Plan" --tail 100
# Run a command block with an az login token for the org's tenant (no PAT exported)
${CLAUDE_SKILL_DIR}/scripts/ado-exec.sh -- bash -c '<commands>'az repos ref delete --name heads/<branch> -r <repo> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az artifacts feed list --org "$ADO_ORG" --detect false
az artifacts feed create --name <name> --org "$ADO_ORG" --detect false
az artifacts feed delete --feed <name> --org "$ADO_ORG" --detect falsePrefer annotated tags (git tag -a): ADO's Tags view shows the tagger, date, and message only for annotated tag objects - a lightweight tag renders with an empty creator and no message. Fetch the remote first so the tag lands on the current commit and existing tags are visible.
git fetch origin --tags
git tag -a v1.2.3 <commit> -m "<repo> v1.2.3: what this release ships"
git push origin v1.2.3These operations have no az CLI support. Use curl with the REST API. See the "Manage Environment Checks & Approvals" workflow recipe below, and reference.md for advanced patterns (branch control checks, deployment records).
# 1. Find pipeline and recent runs
az pipelines list --name "bvp" --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
az pipelines runs list --pipeline-ids 8 --top 5 --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
# 2. Inspect run (stage/job tree)
${CLAUDE_SKILL_DIR}/scripts/ado-get-run.sh --run-id 12345
# 3. Get failed task logs
${CLAUDE_SKILL_DIR}/scripts/ado-get-logs.sh --run-id 12345 --failed-only
# 4. Drill into specific task
${CLAUDE_SKILL_DIR}/scripts/ado-get-logs.sh --run-id 12345 --task "Terraform Plan" --tail 100# List environments
# AUTH as set in the Authentication section (Basic PAT or az Bearer token)
BASE="${ADO_ORG}/${ADO_PROJECT}/_apis"
curl -s -H "$AUTH" "$BASE/distributedtask/environments?api-version=7.1"
# List checks on environment
curl -s -H "$AUTH" "$BASE/pipelines/checks/configurations?resourceType=environment&resourceId=<ENV_ID>&\$expand=settings&api-version=7.1-preview.1"
# Approve a pipeline run
curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
-d '[{"approvalId":"<ID>","status":"approved","comment":"Verified plan"}]' \
"$BASE/pipelines/approvals?api-version=7.1-preview.1"# 1. Show PR details + list active threads
az repos pr show --id <ID> --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
${CLAUDE_SKILL_DIR}/scripts/ado-pr-threads.sh --pr-id <ID> --repo <repo> --status active
# 2. Read a specific thread in full (--json + python3 for formatting)
${CLAUDE_SKILL_DIR}/scripts/ado-pr-threads.sh --pr-id <ID> --repo <repo> --json \
| python3 -c "import sys,json; threads=json.load(sys.stdin); t=[x for x in threads if x['id']==<threadId>]; print(json.dumps(t[0],indent=2)) if t else print('Not found')"
# 3. Reply to reviewer feedback
# AUTH as set in the Authentication section (Basic PAT or az Bearer token)
THREADS="${ADO_ORG}/${ADO_PROJECT}/_apis/git/repositories/<repo>/pullRequests/<ID>/threads"
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST \
"${THREADS}/<threadId>/comments?api-version=7.1" \
-d '{"parentCommentId":1,"content":"Fixed in latest push.","commentType":1}'
# 4. Provide code suggestion
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST \
"${THREADS}?api-version=7.1" \
-d '{"comments":[{"content":"Consider:\n\n```suggestion\nnew_code_here\n```","commentType":1}],"status":"active","threadContext":{"filePath":"/path","rightFileStart":{"line":10,"offset":1},"rightFileEnd":{"line":12,"offset":1}}}'
# 5. Resolve addressed threads
curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH \
"${THREADS}/<threadId>?api-version=7.1" \
-d '{"status":"fixed"}'
# 6. Ask for clarification (create new general thread)
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST \
"${THREADS}?api-version=7.1" \
-d '{"comments":[{"content":"Question: what is the expected behavior when X?","commentType":1}],"status":"active"}'# 1a. Get ticket context (all fields + relations for parent/child)
az boards work-item show --id <WI_ID> --expand all --org "$ADO_ORG" --detect false -o json
# 1b. Load work item comments for context
# AUTH as set in the Authentication section (Basic PAT or az Bearer token)
curl -s -H "$AUTH" -H "Content-Type: application/json" \
"${ADO_ORG}/${ADO_PROJECT}/_apis/wit/workItems/<WI_ID>/comments?\$top=10&order=desc&api-version=7.1-preview.4"
# 2. Get parent story for context (parse parent URL from relations, extract ID)
az boards work-item show --id <PARENT_ID> --expand all --org "$ADO_ORG" --detect false -o json
# Note parent's: custom fields, area-path, iteration-path
# 3a. Pick up existing unassigned task
az boards work-item update --id <WI_ID> --state Active --assigned-to "[email protected]" \
--org "$ADO_ORG" --detect false
# Set custom fields if missing
az boards work-item update --id <WI_ID> \
-f "<custom-field-1>[email protected]" "<custom-field-2>[email protected]" \
--org "$ADO_ORG" --detect false
# 3b. Or create new task from parent story (inherit custom fields, area, iteration)
az boards work-item create --type "<TaskType>" --title "Implement X" \
--description "Description of the task" \
--area "<AreaPath>" \
--iteration "<IterationPath>" \
-f "<custom-field-1>[email protected]" "<custom-field-2>[email protected]" \
--assigned-to "[email protected]" \
--org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
# Link to parent
az boards work-item relation add --id <NEW_TASK_ID> --relation-type parent --target-id <PARENT_ID> \
--org "$ADO_ORG" --detect false
# 4. Do development work...
# 5. Create PR
az repos pr create -r my-repo -s feature/branch --title "Implement X" \
--auto-complete true --squash true --delete-source-branch true \
--org "$ADO_ORG" -p "$ADO_PROJECT" --detect false
# 6. Link work item to PR
az repos pr work-item add --id <PR_ID> --work-items <WI_ID> \
--org "$ADO_ORG" --detect false
# 7. Add reviewers - az repos pr reviewer add fails under PAT auth. Use the
# REST PUT with a vssps identity GUID (see "Reviewer identity resolution").# 1. List teams
az devops team list --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false -o table
# 2. Get members of a team
az devops team list-member --team "<TeamName>" --org "$ADO_ORG" -p "$ADO_PROJECT" --detect false -o table
# 3. Add as PR reviewer - az repos pr reviewer add fails under PAT auth. Resolve
# the member's email to a vssps identity GUID and use the REST PUT (see
# "Reviewer identity resolution").az boards work-item show does not allow --expand and -f (fields) together, and --expand defaults to all. A bare -f fails with The expand parameter can not be used with the fields parameter, so pass --expand none with -f.
Setting --state Active and --assigned-to in the same create call is not supported. Create the task first (it starts as New), then update it in a second call:
# 1. Create
az boards work-item create --type "<TaskType>" --title "..." --description "..." \
--area "<AreaPath>" --iteration "<IterationPath>" \
--org "$ADO_ORG" -p "$ADO_PROJECT" --detect false -o json
# 2. Activate and assign
az boards work-item update --id <NEW_ID> --state Active --assigned-to "[email protected]" \
--org "$ADO_ORG" --detect falseaz repos pr work-item remove frequently no-ops on PR ArtifactLinks - it returns success but the link stays. Remove the link from the work-item side instead: find the index of the ArtifactLink relation, then remove that relation with a JSON Patch:
# 1. Find the relation index (position in .relations[] of the ArtifactLink)
az boards work-item show --id <WI_ID> --expand relations --org "$ADO_ORG" --detect false -o json
# 2. Remove it (AUTH as set in the Authentication section)
curl -s -H "$AUTH" -H "Content-Type: application/json-patch+json" -X PATCH \
"${ADO_ORG}/_apis/wit/workitems/<WI_ID>?api-version=7.1" \
-d '[{"op":"remove","path":"/relations/<idx>"}]'Identity <id> has not been materialized, please use interactive login over the browser first (HTTP 403, AadUserStateException) means the token was issued by a different Entra tenant than the one that backs the org. Universal package commands report the same failure as Failed to update Universal Packages tooling. Under the az login fallback this happens when all your cached subscriptions sit in one tenant and the org sits in another, because the az devops extension then uses that tenant's token without checking it. Run the commands through ado-exec.sh, which mints the token for the org's tenant. If a token from the org's tenant gets the same error, the org has not seen your account yet, so open the org once in a browser or ask an org admin to add you.
Without valid credentials, Azure DevOps REST endpoints answer with a 302 redirect to a sign-in page, and some answer 203 with the sign-in page as HTML. curl treats neither as an error, so a JSON parser fails on the HTML instead. When a response does not parse, check its status with curl -w '%{http_code}'. The bundled scripts treat a 401, a redirect to sign-in, or an HTML page in answer to a read as rejected credentials. They mint a rejected az login token once more, and they stop with a message when a PAT or ADO_TOKEN is rejected.
A build-validation policy evaluation on a draft PR sits queued indefinitely - the build starts only when the PR is published. Do not poll the policy status of a freshly created draft. Either skip the gate (it runs on publish) or queue the evaluation explicitly when a green check is wanted up front:
eval_id=$(az repos pr policy list --id <PR_ID> --org "$ADO_ORG" --detect false \
--query "[?configuration.type.displayName=='Build'].evaluationId | [0]" -o tsv)
az repos pr policy queue --id <PR_ID> -e "$eval_id" --org "$ADO_ORG" --detect falseA push to an already-queued PR re-triggers the evaluation; only then is polling meaningful.
At the start of each session that involves assigning work items or adding PR reviewers, resolve the current user's identity:
# AUTH as set in the Authentication section (Basic PAT or az Bearer token)
curl -sS -H "$AUTH" "${ADO_ORG}/_apis/connectionData" | python3 -c "
import sys, json
u = json.load(sys.stdin)['authenticatedUser']
mail = u.get('properties', {}).get('Account', {}).get('\$value')
print(json.dumps({'displayName': u.get('providerDisplayName'), 'mail': mail, 'id': u['id']}))"Use the returned email for --assigned-to and the GUID for reviewer operations. Azure DevOps answers this call itself, so the result holds for every auth source, including a guest account in the org's tenant. Avoid az ad signed-in-user show for this, because it returns your Entra object id in your home tenant, and reviewer operations need the Azure DevOps identity id. Do not persist this to memory - the user may switch accounts between sessions.
Check CLAUDE.md for project-specific ADO configuration (custom work item types, area paths, iteration paths, custom fields, team conventions).
When the user asks for an ADO operation:
ADO_TOKEN, or az login token through ado-exec.sh); instruct the user only if no source worksaz CLI with -o json when parsing programmatically, -o table for display${CLAUDE_SKILL_DIR}/scripts/ for: create-policy, get-run, get-logs, pr-threads, and exec (az login token for a command block)az boards work-item show/update/relation add are org-scoped (no --project), only create and query take --projectaz repos pr work-item add (from PR side)az command fails unexpectedly, fall back to raw curl `suggestion blocks - ADO renders them as applicable diffsFor detailed reference material and real output examples, consult:
ado-exec.sh keeps az on the exported credential, tenant lookup, mint errors, why not az login --allow-no-subscriptions), native ArtifactTool on Apple Silicon~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.