◀── Back to Logs
JUL 2026 [SYSTEMS] 5 min read

LOG 010.6: Connecting Nine to Azure DevOps — and Getting Credential Custody Right

LOG 010.6: Connecting Nine to Azure DevOps — and Getting Credential Custody Right

This is Part 6 of the Nine / 9OS series. Part 5 closed the original arc: Nine online, operating inside the Paragon9 M365 environment with a locked identity, a SharePoint Command Deck, and four Teams channels enforcing the direction of work. That was supposed to be the final entry.

Nine didn't stop growing. This entry reopens the series, because an agent that can talk, file, and draft, but can't see the code, is only half a team member at a firm that ships software. This entry connects Nine to Azure DevOps and uses that connection to install something the series has needed since the first client secret was created: a formal credential custody model, built on Bitwarden Secrets Manager as the vault of record.

Series hub: LOG 010: Building Nine — A Headless AI Agent Operating Layer for Paragon9


01. THE SYSTEMS SOURCE: THE CODE LAYER

By the end of Part 5, Nine had reach into every communication surface that matters: Teams for inbound work, SharePoint for deliverables, Outlook for drafts. What it couldn't see was the thing Paragon9 actually sells: nearly two decades of source code living in Azure DevOps. Client systems, internal tooling, frameworks, one-offs.

That blindness has a real cost. Ask Nine to estimate a migration and it reasons from a description of the codebase instead of the codebase. Ask it to review a change and it reviews a summary. Every answer is secondhand. Connecting the code layer converts Nine's code work from recollection to inspection, an estimate grounded in the actual repo, not the memory of it.

But the code layer raises the stakes on a problem that has been accumulating quietly since Part 5. Every integration Nine gains is a credential: the Graph client secret, and now an Azure DevOps PAT. Each one was created in a portal that shows you the value exactly once, and each one ends up in some file on the OpenClaw machine. Without a custody model, "where do the secrets live?" has a different answer for every secret, which means rotation is guesswork and revocation is archaeology.

So this entry installs two things at once. The connection: a scoped Azure DevOps PAT under Nine's own identity. And the custody model: Bitwarden Secrets Manager is the vault of record, and secrets reach the machine one of two ways: resolved live out of the vault where OpenClaw supports it, or held as chmod 600 runtime copies where a tool insists on a file. Master in the vault, working copies scoped and disposable, nothing anywhere else. Every credential in this series, the Graph secret from Part 5 included, gets retrofitted into that model.

The access philosophy is the same one that governed the Graph permissions in Part 5. Nine does not borrow its principal's identity. The PAT is created under Nine's own account, scoped to read, attributable in the audit trail, and revocable with one click. A scoped team member, not a super-user. And the same review gate applies on the way back out: Nine reads code freely, but it never merges. Branches, commits, and pull requests only. A human merges.

The Insight: Every new integration is a new credential, and the time to decide where that credential's master copy lives is before you create it, because the portal will show you the value exactly once. Custody is not a storage detail. It is the difference between rotation being a five-minute procedure and being an incident.


02. THE BUILD SOURCE: THE DEPLOYMENT ROADMAP

Phase 1: Decide the Access Model

Before touching the portal, settle three questions:


Phase 2: Establish the Vault of Record in Bitwarden Secrets Manager

Do this before creating the PAT, so the vault entry is waiting when the portal shows you the value.

The vault of record is Bitwarden Secrets Manager — not the consumer password manager, its machine-facing sibling. The distinction matters for an agent: Secrets Manager is built for non-interactive access. You create a machine account, scope it to a project, and issue it an access token that is itself scoped, expiring, and revocable. The free tier covers everything this series needs. No browser extension, no master-password prompt on a headless box — a CLI (bws) that a daemon can call.

Set up the vault side in the Bitwarden web console:

  1. Create a Secrets Manager project — 9os
  2. Create a machine account for the OpenClaw host, grant it access to the project
  3. Generate an access token for the machine account — this is the one credential that bootstraps all the others

Then the machine side:

# Install the bws CLI (single binary from github.com/bitwarden/sdk-sm releases)
mkdir -p ~/.local/bin
# download, extract, place at ~/.local/bin/bws

# The bootstrap token lives in one chmod-600 env file
cat > ~/.openclaw/bws.env << 'EOF'
BWS_ACCESS_TOKEN=YOUR_MACHINE_ACCOUNT_TOKEN
BWS_BIN=/home/paragon9/.local/bin/bws
EOF
chmod 600 ~/.openclaw/bws.env

# Smoke test
. ~/.openclaw/bws.env && $BWS_BIN secret list

Create the secret for the PAT you're about to generate. Use slash-path keys, a naming convention that scales as the vault grows:

That note field is the custody model doing its job: every vault entry records where its runtime copies live, so rotation and revocation are a checklist instead of a search.

The vault supports two tiers of custody, and it's worth being precise about which secret gets which:

Tier 1 — resolved live. OpenClaw can resolve config secrets straight out of the vault at startup via SecretRefs: the config holds a reference, a small resolver script calls bws, and the actual value never exists as plaintext in openclaw.json at all. The Teams app password from Part 2 and other OpenClaw-native API keys migrate to this tier — no plaintext at rest.

Tier 2 — vault-mastered runtime copies. Some secrets are consumed by tools that insist on reading a file: git wants ~/.git-credentials, the Graph scripts read ~/.openclaw/msgraph.json. These stay as chmod 600 files on disk — but their master lives in the vault, and the vault note records the file path. The ADO PAT is Tier 2.

While you're in the vault, retrofit the Part 5 credentials the same way: a 9os/graph/client-secret entry for the 9OS-OpenClaw Graph app, noting ~/.openclaw/msgraph.json as its runtime copy.

⚠️ Critical: If you migrate OpenClaw config fields to Tier 1 SecretRefs, validate the config before restarting the gateway (openclaw config validate). Migrating a field the SecretRef surface doesn't support aborts gateway startup and the automated repair path (doctor --fix) recovers by reverting every SecretRef back to plaintext, undoing the whole migration. Validate first, restart second. We learned this the hard way.

⚠️ Critical: The vault is the only place a master value lives. Secrets never go in openclaw.json as plaintext, never in the workspace, never in a repo, never in a Teams message. The machine files are disposable copies — if the box dies, you rebuild from the vault, not the other way around.

One honest boundary: SecretRefs and vault custody protect against plaintext at rest, leaked backups, and unrotatable sprawl. They do not hide values from a shell-capable agent on the same host. The gateway holds the access token, and anything that can run commands as that user can use it. Host-level isolation is a sandboxing problem, not a vault problem. Know which threat you're solving.


Phase 3: Create the PAT in Azure DevOps

Signed in as Nine's account at https://dev.azure.com/<your_org>:

User settings (top right) → Personal access tokens → + New Token

Click Create. Copy the token value into the 9os/azure-devops/nine-pat secret from Phase 2 immediately — same one-time-display rule as the Graph client secret in Part 5. Once you navigate away, it is gone permanently.


Phase 4: Store the Runtime Copy on the OpenClaw Machine

Git needs the credential for clone and fetch operations. Use git's store helper with a pre-seeded credentials file:

git config --global credential.helper store

cat > ~/.git-credentials << 'EOF'
https://nine:[email protected]
EOF

chmod 600 ~/.git-credentials

Replace YOUR_PAT_VALUE with the token from the vault. The username portion (nine) is cosmetic — Azure DevOps authenticates PATs regardless of the username — but it makes the credential attributable at a glance when you audit the file later.

⚠️ Gotcha #12 — Git hangs forever on a headless box with no seeded credential

On a desktop, a missing git credential produces a username/password prompt. On a headless daemon, that same prompt has no terminal to appear in. The clone just hangs until the sub-task times out, and nothing in the output says "authentication" anywhere. If Nine reports that a clone stalled or timed out with no error, check ~/.git-credentials exists, is chmod 600, and contains the dev.azure.com line before debugging anything else. Pre-seed the store; never rely on interactive auth on a machine with no human attached.


Phase 5: Test the Git Path

Clone something small and confirm the round trip:

cd /tmp
git clone https://dev.azure.com/paragon9/YOUR_PROJECT/_git/YOUR_REPO
ls YOUR_REPO
rm -rf YOUR_REPO

No prompt, no hang, files on disk. The git path is live.


Phase 6: Test the REST Path

Git access covers cloning. Discovery — "what exists in this organization?" — is a REST API question. Azure DevOps accepts the PAT as HTTP Basic auth with an empty username:

PAT=$(grep -o '://nine:[^@]*' ~/.git-credentials | cut -d: -f3)

curl -s -u ":$PAT" \
  "https://dev.azure.com/<your_org>/_apis/projects?api-version=7.1&\$top=200" \
  | python3 -c "
import json, sys
data = json.load(sys.stdin)
print('Projects:', data['count'])
for p in data['value'][:10]:
    print(' -', p['name'])
"

Expected output: a project count and the first ten names. Nine can now enumerate the entire organization programmatically — projects, repos per project, and file trees — without cloning anything.

⚠️ Gotcha #13 — Project count is not code count

Out of the number of projects your organization reports, how many of them actually contain code? How many are just empty shells from years of project-first workflows where the repo never materialized? If Nine's discovery pass naively iterates projects and clones "everything," it burns time and disk on nothing. Enumerate repositories within each project (_apis/git/repositories) and check for a non-zero default branch before treating a project as real. Organizations accumulate empty projects the way garages accumulate boxes; teach the agent to check before lifting.


Phase 7: Set the Operating Rules for the Code Layer

Access without discipline is how an agent fills a disk or leaks a repo. Three rules, written into Nine's standing instructions:

Clone, inspect, delete. The OpenClaw machine has limited disk. Nine clones into a scratch path (/tmp), extracts what the task needs, and removes the clone before moving to the next repo. No resident mirror of the org.

⚠️ Gotcha #14 — Repo scans that keep clones resident will fill the host

Over a decade of real repos, some of them years deep, do not fit comfortably on a small root partition alongside everything else this series has installed. The failure mode is nasty because it's delayed: the scan works fine, then a week later something unrelated fails with a disk-full error. The clone-inspect-delete discipline isn't tidiness, it's what keeps the code layer from taking down the rest of the machine.

Read is not publish. Anything derived from a client repo — summaries, excerpts, candidate lists — stages to the Command Deck for review before it goes anywhere. Nine flags, a human decides.

Nine never merges. Same principle as Part 5's operating contract, now enforced at the scope level: today's PAT can't write at all, and when write access arrives, it arrives as branch-and-PR, never direct-to-main. The review gate is structural, not behavioral.


03. THE LEGACY SOURCE: NINE CAN SEE THE CODE

The stack, extended:

Nine (code operations)
  → NordVPN tunnel (dedicated static IP)
    → Azure DevOps (dev.azure.com/paragon9)
      → PAT: Nine's identity, Code: Read
        → git clone (scratch, clone-inspect-delete)
        → REST API (org-wide discovery, no clone needed)
          → Analysis staged to Command Deck → human review

Credential custody (all integrations)
  Bitwarden Secrets Manager (vault of record — machine account, scoped token)
    ├─ Tier 1, resolved live: openclaw.json SecretRefs via bws resolver
    │    (Teams app password, API keys — no plaintext at rest)
    └─ Tier 2, vault-mastered runtime copies (chmod 600, path recorded on entry):
         ~/.openclaw/msgraph.json  (Graph client secret)
         ~/.git-credentials        (Azure DevOps PAT)

What this unlocked, immediately: org-wide discovery across all projects in one pass, and code work that starts from the repository instead of a description of it. A migration estimate Nine produced after this connection was grounded in the actual data layer of the actual codebase (tables, sync jobs, the property that wasn't in the model) instead of an educated guess dressed as one. That is the difference between an agent that talks about code and an agent that has read it.

And the custody model means the growing credential count is an asset inventory instead of a liability sprawl. Every secret has one master, one recorded runtime location, one expiration date with a reminder on it, and one revocation point.

The Insight: Handing an agent your source code is the largest trust decision in this series so far — bigger than email, bigger than SharePoint. What makes it safe is not hoping the agent behaves. It's the same structure that made everything else in 9OS safe: a scoped identity, a minimal grant, an audit trail with a name on it, a review gate on everything outbound, and a credential you can kill in one click because you always knew exactly where it lived.


[MAINTENANCE NOTES]

PAT rotation (at expiration, or immediately on suspicion):

  1. Create the new PAT in Azure DevOps under Nine's account
  2. Update the 9os/azure-devops/nine-pat secret in Bitwarden (new value, new expiration in the note)
  3. Update ~/.git-credentials on the OpenClaw machine
  4. Revoke the old PAT in the portal
  5. Reset the calendar reminder

The order matters: vault before machine, so the master is never behind the copy. Tier 1 secrets rotate in one step fewer — update the vault value and restart the gateway; there is no machine file to touch.

Machine-account token rotation: the bws access token in ~/.openclaw/bws.env is itself a credential with an expiration. Rotate it in the Bitwarden console, update the env file, restart the gateway. This is the bootstrap secret so treat its expiry reminder as non-optional.

Status: Nine can see the code. The custody model is installed. Every credential has one home.


[SYSTEM NOTES]

Stay Resilient.

— Greg Scott Kirk (Tek)


Part 6 of the Nine / 9OS series.
← Previous: LOG 010.5 — Connecting OpenClaw to Microsoft Graph and Bringing Nine Online
Series hub: LOG 010 — Setting Up a Dedicated M365 Identity for Nine
Up next: LOG 010.7: Growing Nine into a team — the sub-agent roster.

// NEXT ACTION

This is the architecture Paragon9 installs for mid-market firms — modernized, documented, and built to run without its operator. If you want it running in your business, the conversation is free.