Skip to content
GitHub

Credentialed skill walkthrough

Look up every broker, step, and rule for building a credentialed skill, from frontmatter to lint.

This walkthrough is for when your skill calls an external authenticated service (API token, vendor CLI, or corporate SSO) and the credential must never pass through the model. You need: target service’s auth shape identified, namespace name chosen, and the credential-brokers pack installed if you’re using the creds or sso-cookie broker. You end with: a scaffolded, lint-passing credentialed skill directory with the correct broker wired, a ### Security rules (non-negotiable) block in SKILL.md, and banded exit-code handling in scripts/cli.py.

This is a one-page walk-through for authoring a credentialed primitive — a skill that calls an authenticated external API on behalf of the user. The architecture rule is skills don’t hold credentials; a Python CLI under the skill’s scripts/ directory owns the secret on disk and constructs the API call inside its own process. The LLM never sees the token as a tool argument.

For a runnable, shipped reference, read a real consumer — packs/atlassian/.apm/skills/jira/ is a live auth: creds credentialed-CLI whose scripts/_client.py resolves a PAT via the credbroker library; this guide is the procedure that gets you to your own.

You need:

  • The target service’s authentication shape — static API token, vendor CLI, or corporate-SSO session cookie. The broker you pick depends on this.
  • A namespace name — a short kebab/snake-case identifier (jira, github, acme_corp). For static tokens this becomes the env-var prefix and the keychain account label.
  • The credential-brokers user-scope pack installed (agentbundle install --pack credential-brokers --scope user), if you’re using the creds or sso-cookie broker.

metadata.auth names the broker that resolves the credential. Four ids, picked once per skill:

  • env — the credential is a plain environment variable (<NAMESPACE>_<KEY>). Catalogue contributes naming convention and lint; no runtime resolver. Pick this for CI runners, ephemeral containers, and adopters whose threat model permits process env.
  • cli — the primitive shells out to a vendor-authenticated binary (gh, aws, kubectl, gcloud). Vendor CLI owns the credential. Pick this when the user has already authenticated the vendor binary on their PATH.
  • creds — static token resolved via the three-tier model (env → OS keychain → 0600 dotfile floor). Resolution comes from the credbroker library (pip install credbroker), imported in-process — declare it in your skill’s requirements.txt (Step 9). Pick this for static API tokens / PATs.
  • sso-cookie — session cookie acquired via a browser SSO flow. Your skill imports credbroker and calls load_sso_cookies / refresh_sso_session; the library subprocess-invokes the sso-broker.py engine on your behalf. Pick this for corporate-SSO endpoints (e.g. enterprise Jira / Confluence behind Okta or AzureAD).

The rest of this guide picks creds as the worked example because it’s the most common case. The verbatim per-broker ### Security rules (non-negotiable) block you embed in your SKILL.md is given inline in Step 7, one per broker; copy the one matching your choice.

Pick a primitive class (orthogonal to broker)

Section titled “Pick a primitive class (orthogonal to broker)”
  • credentialed-cli — your primitive is a Python CLI invoked from the skill body via subprocess.run([sys.executable, "scripts/cli.py", ...]). The argv ban applies (no --token / --api-token / --bearer / --pat / --password flags) regardless of broker.
  • mcp-server — your primitive is a long-lived MCP server the user wires into their MCP host configuration. Header-naming flags (--bearer-header, --auth-header, --header-prefix) are allowed; the storage convention does not apply because the server holds no on-disk credential state.

The rest of this guide assumes credentialed-cli (the common case).

your-skill-name/
├── SKILL.md
├── scripts/
│ └── cli.py
└── references/
└── creds-schema.toml # only for auth: creds / auth: env

Place this under your pack’s .apm/skills/ directory (e.g. packs/<your-pack>/.apm/skills/your-skill-name/).

The frontmatter shape varies by broker. For auth: creds:

---
name: your-skill-name
description: <one-line description; what triggers the skill>
metadata:
credentialed: true
primitive-class: credentialed-cli
auth: creds
namespace: your-namespace
keys: ["API_TOKEN"]
---

For auth: env: same shape, auth: env. For auth: sso-cookie: auth: sso-cookie plus sso_profile: <profile> (no namespace/keys). For auth: cli: just auth: cli (no namespace/keys/profile).

agentbundle catalogue verify --root . refuses unknown auth: values; metadata.credentialed: true requires metadata.auth.

Declare the schema (auth: creds and auth: env only)

Section titled “Declare the schema (auth: creds and auth: env only)”

The schema lives at <skill-dir>/references/creds-schema.toml:

[namespace]
name = "your-namespace"
[[namespace.keys]]
name = "API_TOKEN"
label = "<service> API token"
secret = true
[[namespace.keys]]
name = "BASE_URL"
label = "<service> instance base URL"
secret = false

secret = true keys are prompted via getpass.getpass (no echo); secret = false keys are prompted via input(). auth: cli and auth: sso-cookie skip this step entirely.

For auth: creds — declare credbroker in your skill’s requirements.txt (Step 9) and import it directly:

from credbroker import (
CredentialsMissingError,
Tier2HardFailError,
load_credentials,
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="your-skill-name")
parser.add_argument("verb", choices=("call", "check"))
args = parser.parse_args(argv)
try:
creds = load_credentials(
"your-namespace",
required_keys=["API_TOKEN", "BASE_URL"],
)
except CredentialsMissingError as exc:
sys.stderr.write(f"{exc}\n")
sys.stderr.write(
"run the `credential-setup` skill to set the missing keys\n"
)
return 2 # EXIT_USER_ACTION — the user must act
except Tier2HardFailError as exc:
sys.stderr.write(f"keychain unavailable: {exc}\n")
return 1 # EXIT_ERROR — functional; the message carries the cause
# `creds.API_TOKEN` and `creds.BASE_URL` are attribute-accessible
# strings. Never print them, log them, or echo them.
...

credbroker’s stdlib core pulls no third-party dependency; the optional credbroker[crypto] extra adds an encrypted-at-rest vault (Argon2id → AES-256-GCM) as the Tier-3 floor where it’s installed. The architectural rule above still applies: cleartext stays inside your interpreter’s process boundary.

Use the banded exit codes: 0 ok, 1 functional/operational error (the catch-all bucket; the message carries the cause), 2 the user must act (credentials, 401/403, a missing dependency), with 3–9 reserved for future credential/auth codes. Then wrap your entry point in a top-level except Exception so no failure escapes as a traceback — Tier2HardFailError and anything unexpected map to 1 (print the exception type, never str(exc), on the unexpected path). Do not use except BaseException: SystemExit (your own input-validation exits) and KeyboardInterrupt (130) must pass through.

For auth: env — just os.environ["<NAMESPACE>_<KEY>"]. The lint asserts at least one read per declared key.

For auth: cli — subprocess.run(["<vendor-cli>", ...], env={**os.environ}). The vendor CLI owns the credential.

For auth: sso-cookie — call credbroker. Never resolve the broker path, build its argv, or call subprocess from a skill script:

import credbroker
credbroker.validate_sso_profile(profile) # grammar guard
jar_path = credbroker.load_sso_cookies(profile) # a path, never bytes
# Re-establish an expired session, without a human. Takes only a profile —
# the signature is structurally incapable of carrying a sign-in destination,
# which is what stops an automated path choosing where the browser goes. It
# runs headless: if the browser profile cannot complete the flow unaided it
# raises rather than putting a login page in front of whoever is at the
# machine.
credbroker.refresh_sso_session(profile)
# First capture. The only function that accepts a destination — reach it only
# from an operator-typed action, never automatically.
credbroker.register_sso_session(
profile,
login_url=...,
success_url_pattern=...,
cookie_domains=(...,),
validation_endpoint=...,
)
# Optional: ask the resource server where it sends users to sign in, and
# compare before opening a browser. Defence in depth, not a control — the
# derivation target lives in the same config file as the value it attests.
credbroker.derive_sso_destination(base_url, strategies=("atlassian-seraph",))

The resolver emits the path to a serialised cookie jar; load it inside your primitive and construct the authenticated request without surfacing cookie values to the LLM.

Keeping the spawn inside credbroker is not tidiness. The wall-clock bound, the whole-process-tree kill (POSIX process groups vs Windows taskkill), and the environment allowlist that stops a headed browser inheriting your *_API_TOKEN are written once there, type-checked and CI-exercised — in a skill script they would be neither, and they would be copy-pasted into the next consumer.

Exit codes you must distinguish. refresh returns 4 when the profile was never registered — route the operator to a first capture — and 5 when a person has to sign in. Both are exit-2 territory for your CLI, but they carry different remediations. Everything else the engine returns is an internal failure, not “your session expired”: treat only the typed session-unavailable signal as recoverable, or a slow keychain will trigger a browser recapture while the stored session is perfectly valid.

Embed the Security-rules block in SKILL.md

Section titled “Embed the Security-rules block in SKILL.md”

Every credentialed skill carries a ### Security rules (non-negotiable) block in its SKILL.md body. Copy the block matching your broker verbatim — the lint (tools/lint-credentialed-skills.sh) pins the heading and the broker-specific phrases, so a skill missing either ships as a lint finding. Substitute the placeholders (<namespace>, <KEY>, <NAMESPACE>_<KEY>, <vendor-cli>, <sso-profile>) for your service; leave the rest byte-for-byte.

auth: creds:

### Security rules (non-negotiable)
- Secrets live only in `~/.agentbundle/credentials.env`
(mode 0600 on POSIX; DACL-restricted on Windows), the OS keyring,
or process environment variables.
**Never** read that file, print it, or echo the token.
- **Never** put the token on the command line. The primitive
refuses flags like `--token` / `--api-token` / `--bearer` /
`--pat` / `--password` and exits — do not work around it.
- If `check` exits with the "missing credentials" code, tell the
user to run the `credential-setup` skill themselves. It's
interactive — do not run it for them.

auth: env:

### Security rules (non-negotiable)
- Secrets live only in the process environment. **Never** print, log, or
echo the value of `<NAMESPACE>_<KEY>`.
- **Never** put the credential on the command line. The primitive
refuses flags like `--token` / `--api-token` / `--bearer` /
`--pat` / `--password` and exits — do not work around it.
- If the env var is missing, tell the user to export
`<NAMESPACE>_<KEY>` in their shell rc (or the equivalent for their
process manager) and re-launch the session. Do not write the value
anywhere yourself.

auth: cli:

### Security rules (non-negotiable)
- Secrets live only in the vendor CLI's auth store. **Never** read
that store, print it, or echo the token.
- **Never** put the token on the command line. The primitive
refuses flags like `--token` / `--api-token` / `--bearer` /
`--pat` / `--password` and exits — do not work around it.
- If the vendor CLI exits with an authentication error, tell the
user to run the vendor's auth flow themselves (e.g.
`<vendor-cli> auth login`). It's interactive — do not run it for
them.

auth: sso-cookie:

### Security rules (non-negotiable)
- Secrets live only in cookie jar in OS keychain (mode 0600 on POSIX;
DACL-restricted on Windows). **Never** read the jar file directly,
print its contents, or echo cookie values.
- **Never** put a session cookie on the command line. The broker
refuses flags like `--token` / `--api-token` / `--bearer` /
`--pat` / `--password` and emits only a *path* on stdout — do not
parse the jar yourself.
- If the broker exits with the "re-auth required" code (`2` from
`get-cookies`, or `4` from `refresh` when no profile was ever
registered), tell the user the SSO session has expired and that
capturing a new one opens a browser. It's interactive — do not run
any setup helper for them.

Write the operational body: bootstrap and failure handling

Section titled “Write the operational body: bootstrap and failure handling”

Step 7 gives the agent the prohibitions. The agent also needs the operations: how to get the skill working on first run, and what to do when a call fails. Embed the two sections below in your SKILL.md body. Unlike the Security-rules block, these are recommended, not lint-pinned — keep the shape, adapt the wording to your service.

The dividing line is the charter rule: the agent may install its own non-secret prerequisites, but never enters the credential itself. Self-bootstrap covers pip install; credential entry stays user-invoked (Step 7, Step 10).

### Verify the environment
Install dependencies (idempotent — safe to re-run), then check auth:
python -m pip install -r requirements.txt
python scripts/cli.py check
- Exit 0 → authenticated; proceed.
- Exit 2 → credential missing, unresolved, or rejected (401/403) → see
*When a request fails*.
- Any other non-zero → read the stderr message. `ModuleNotFoundError:
credbroker` means the resolver isn't installed — run `python -m pip
install -r requirements.txt` (or `pip install credbroker`). Report it;
don't patch around it.
### When a request fails
The CLI exits non-zero and writes the cause to stderr. Read the message — it
names the cause more reliably than the exit code, whose meaning varies
between skills. Act on the cause:
- **Credential missing, unresolved, or expired** (often surfaced as a 401).
Have the user run the setup action for this broker (below); it's
interactive — do not run it for them. Re-run `check`; proceed only when it
exits 0.
- **403 — authenticated but forbidden.** A scope/permission gap, not a
missing token. `creds` / `env`: have the user regenerate the *same*
credential with the missing scope and re-run setup — don't create a second
credential. `cli`: the vendor's re-auth with scopes
(`gh auth login --scopes …`). `sso-cookie`: usually a missing entitlement
on the SSO account — report it; re-running `get-cookies` won't fix it.
Don't retry blindly.
- **Environment problem** — a keychain/Tier-2 hard-fail, or `credbroker`
not installed (`ModuleNotFoundError: credbroker`). Run `python -m pip
install -r requirements.txt`; report it.
- **Upstream 5xx or rate limit.** Report the message; don't loop.

“The setup action” resolves per broker — the same one you document in the credential step (Step 10):

  • creds → run the credential-setup skill (interactive; the user runs it).
  • env → export <NAMESPACE>_<KEY> in the shell rc and re-launch.
  • cli → run the vendor’s auth flow (gh auth login, aws configure, …).
  • sso-cookie → the next get-cookies opens a browser; let the user complete it.

Declare the credbroker dependency (auth: creds only)

Section titled “Declare the credbroker dependency (auth: creds only)”

Add credbroker to your skill’s requirements.txt (beside httpx if you use it), then install:

credbroker
python -m pip install -r requirements.txt

The credbroker library is pip-installable and imported in-process, so there is no make build-self projection step for the resolver and no scripts/-vendored shim to keep in sync. In a fresh repo checkout, before either the floor or a pip install is in place, from credbroker import … fails with ModuleNotFoundError: credbroker. For local development, install from the repo path: python -m pip install -e ./packages/credbroker. For locked-down sites, see Installing without PyPI (corporate) just below.

How credbroker reaches sys.path — the layered model

Section titled “How credbroker reaches sys.path — the layered model”

import credbroker resolves through a sys.path precedence stack fed by three delivery layers, built in cost/value order. You don’t choose between them — they stack, and the highest-precedence one present wins:

  1. Vendored floor (zero-pip, always present at user scope). When a user installs the credential-brokers pack at user scope (agentbundle install --pack credential-brokers --scope user), install delivers a byte-faithful, stdlib-base copy of the package source to ~/.agentbundle/lib/credbroker/, and every credentialed skill appends ~/.agentbundle/lib to sys.path at lowest precedence (the five API CLIs inside their __package__ bootstrap; credential-setup’s setup.py ahead of its top-level import). So a no-repo user-scope install resolves import credbroker — and full env→keyring→dotfile (Tier-1/2/3) resolution — with no pip at all. The floor is stdlib-only, so its Tier-3 dotfile is plaintext; the encrypted [crypto] vault is not available from the floor alone (layer 2/3 below adds it).
  2. Offline / local pip (corporate, no PyPI). A pip install of the wheel (from an internal index or a local .whl) lands credbroker in site-packages, which sits earlier on sys.path than the floor — so it wins over the floor and unlocks the [crypto] vault. No PyPI dependency. Detailed just below.
  3. PyPI (open adopters). pip install credbroker[crypto] from public PyPI; same site-packages precedence as layer 2. Published as credbroker 0.1.0 (2026-06-10) via the gated OIDC job described under Installing without PyPI below.

Because the bootstrap appends (never prepends) the floor, a pip-installed credbroker of any vintage always shadows it: pip is the primary contract, the floor is the fallback that guarantees resolution where pip hasn’t run. Declaring credbroker in your requirements.txt (above) stays the right thing to do — it covers the local dev loop and non-user-scope installs, and it’s what makes [crypto] reachable.

Starting with AgentBundle 0.51.0, credbroker is a declared wheel-level dependency of agentbundle itself. When a user installs agentbundle from a wheel, pip also installs credbroker 0.7.x. This has no effect on skill scripts and makes no change to the layered model above.

A skill’s requirements.txt still declares credbroker independently. The reason is boundaries: agentbundle installs packs and resolves catalogues; skill scripts run as standalone Python subprocesses — often in venvs or under Python interpreters that have no relation to the one that runs agentbundle. An agentbundle dependency in one Python environment does not make credbroker importable in another.

If your skill script runs in the same environment as agentbundle (for example, during a test), credbroker may already be resolvable through agentbundle’s transitive dependency. Do not rely on this: declare the dependency in your own requirements.txt. An explicit declaration is the only path that works across all Python-environment shapes — venv, editable clone, vendor floor, and the pip install credbroker corporate path.

credbroker does not require PyPI. The release-credbroker workflow builds a platform-independent wheel (credbroker-<version>-py3-none-any.whl) and an sdist on every change to the package and validates them with twine check, so a locked-down or air-gapped site can install from a wheel it hosts or copies in:

  • From an internal package index (Artifactory, Nexus, a private mirror):

    python -m pip install credbroker --index-url https://pypi.example.corp/simple
    python -m pip install "credbroker[crypto]" --index-url https://pypi.example.corp/simple # + encrypted-at-rest vault
  • From a local .whl:

    pip install ./credbroker-<version>-py3-none-any.whl
    pip install "./credbroker-<version>-py3-none-any.whl[crypto]" # + encrypted-at-rest vault

The base wheel has no third-party dependency, so its local-.whl install is the only truly network-free path. The [crypto] extra additionally needs cryptography and argon2-cffi to be resolvable — from the same internal index, or pre-staged alongside the wheel — so on a fully air-gapped host stage those two first.

Either way pip lands the package in site-packages, so the resolver imports exactly as the worked example above — no code change. Public PyPI is wired into the same workflow behind a gated, tag-triggered OIDC job; credbroker 0.1.0 was published to PyPI on 2026-06-10, so pip install credbroker now resolves from public PyPI as well. The internal-index and local-wheel paths above remain available and need no PyPI.

Once everything is in place, populate the credential for your namespace:

  • auth: creds — invoke the credential-setup skill (ships with the credential-brokers pack). It reads your creds-schema.toml, prompts for each required key (secret keys via getpass; non-secret via input), and writes to the highest-available tier (keyring on Darwin/Windows; dotfile on Linux with --allow-insecure-fallback).

  • auth: env — export <NAMESPACE>_<KEY> in your shell rc.

  • auth: cli — run the vendor’s auth flow (gh auth login, aws configure, …).

  • auth: sso-cookie — register the SSO profile. Give your skill a first-run command that calls credbroker.register_sso_session (the jira skill’s is python scripts/jira.py check --register), and have the user run it. Direct engine invocation stays available for a scripted pre-bake:

    python3 ~/.agentbundle/bin/sso-broker.py register your-profile \
    --login-url <login-url> --success-url-pattern <pattern>

Registration opens a headed Chromium window and saves the cookie jar to the OS keychain (or a 0600 file on Linux). After that, refresh re-establishes an expired session headlessly, so a check-style verb can self-heal without a human — see the sso-cookie broker section of docs/architecture/credentials.md.

agentbundle catalogue verify --root . # frontmatter schema
bash tools/lint-credentialed-skills.sh # credentialed-skill rules

agentbundle catalogue verify --root . validates the nested metadata.credentialed, metadata.primitive-class, and metadata.auth keys. lint-credentialed-skills.sh walks every credentialed skill and reports broker-agnostic findings (Don’t-block presence; argv-ban flags; plaintext dotfile reads without opt-out) plus broker-specific findings:

  • auth: creds — refuse if scripts/ imports no credential resolver (from credbroker import …, or the legacy from .credentials_shim).
  • auth: env — refuse if any declared <NAMESPACE>_<KEY> is never read in scripts/.
  • auth: sso-cookie — refuse if scripts/ does not reach the broker through credbroker; refuse hard-coded absolute paths; refuse inline Playwright.
  • auth: cli — no positive-grep enforcement; broker-agnostic checks only.

Both lints exit 0 against the worked example; aim for the same.

  • Printing creds.API_TOKEN inside a debug print(...). The token reaches stdout where any caller can capture it. Use len(creds.API_TOKEN) only if you must prove resolution, and ideally don’t even disclose the length.
  • Forgetting to declare credbroker in requirements.txt. The from credbroker import … resolver import fails with ModuleNotFoundError: credbroker until the dependency is installed (python -m pip install -r requirements.txt).
  • Resolving the SSO broker path yourself at all. Not just the hard-coded-absolute-path version — any skill-side Path.home() / ".agentbundle" / … plus subprocess.run(...). Call credbroker, which owns the path, the wall-clock bound, the process-tree kill and the environment allowlist. A skill script that spawns the engine directly re-implements four cross-platform controls, none of them type-checked or CI-exercised.
  • Treating every broker failure as “the session expired”. A timeout, a missing engine, or an internal broker error is not an expired session; keying recovery on them opens a browser while the stored session is perfectly valid. Recover only on the typed session-unavailable signal.
  • Adding a --token flag “just for local testing”. The argv ban applies in every environment and every broker; the credential-setup skill is the supported escape hatch for the creds broker.

Skills that call external authenticated APIs follow a tighter set of rules than plain skills, because the moment a credential reaches the LLM as a tool argument the architecture has already failed. This section is the in-loop reminder of the shape every credentialed skill must respect.

Skills do not hold credentials. A credentialed primitive — a Python module, an MCP server, or a CLI wrapper packaged as a primitive — owns the secret on disk and constructs the API call inside its own process. The skill body invokes the primitive without ever touching the token. A how-to on adding a credentialed skill walks authors through broker selection and the verbatim security-rules blocks; the shipped jira / figma skills are runnable references.

A credentialed skill declares three project-specific flags under the metadata: block of its SKILL.md frontmatter:

---
name: your-skill-name
description: <what triggers it>
metadata:
credentialed: true
primitive-class: credentialed-cli # or mcp-server
auth: creds # env / cli / creds / sso-cookie
# auth-fallback: creds # optional: dual-auth — the broker to fall
# # back to when the active one can't resolve
# # (e.g. sso-cookie with a creds fallback)
# broker-specific extras follow:
# namespace: <ns> # required for auth: creds and auth: env
# keys: ["<KEY>"] # required for auth: creds and auth: env
# sso_profile: <profile> # required for auth: sso-cookie
---

The keys live under metadata: rather than at top level because the agentskills.io specification pins the top-level frontmatter set to name, description, license, compatibility, metadata, allowed-tools and reserves metadata: as the project-specific escape hatch. agentbundle catalogue verify (step 11) refuses any top-level key outside that set; agentbundle catalogue lint (_PackRules._check_credentialed_skills) scopes its checks to skills with metadata.credentialed: true.

metadata.auth-fallback is optional and names a second broker a dual-auth skill falls back to when the active one can’t resolve (e.g. an auth: sso-cookie skill that drops to creds on a non-SSO instance). When present, the skill’s Security section must satisfy both brokers’ don’t-block phrase sets.

metadata.auth names the broker that resolves the credential. Choose exactly one of these four ids:

  • env — the credential is a plain environment variable (<NAMESPACE>_<KEY>). Catalogue contributes naming convention and lint; no runtime resolver.
  • cli — the primitive shells out to a vendor-authenticated binary (gh, aws, kubectl, gcloud). Vendor CLI owns the credential.
  • creds — static token via the three-tier model (env → OS keychain → 0600 dotfile floor). Resolved via the credbroker library (pip install credbroker), imported in-process; the build-projected credentials_shim it replaced is retired for creds consumers (the four-broker taxonomy above is unchanged).
  • sso-cookie — session cookie acquired via a headed-browser SSO flow. The skill resolves the session through the credbroker SSO resolver (from credbroker import load_sso_cookies), which subprocess-invokes ~/.agentbundle/bin/sso-broker.py (projected by the credential-brokers pack at user scope) — mirroring how creds moved broker resolution into credbroker. A skill that still resolves the broker in its own scripts/ is also accepted.

The broker-agnostic invariants below apply to every credentialed primitive regardless of broker. Broker-specific lint extensions layer on top (auth: creds requires a credential-resolver import in scripts/ — from credbroker import …, or the legacy from .credentials_shim …; auth: env requires each declared <NAMESPACE>_<KEY> to be read at least once; auth: sso-cookie requires either a credbroker SSO import (from credbroker import load_sso_cookies) or subprocess-invocation of the standard Path.home() / ".agentbundle" / "bin" / "sso-broker.py" path; auth: cli falls through to broker-agnostic checks only).

Credentials resolve in this order, first-hit-wins per key:

  1. Tier 1 — env var. <NAMESPACE>_<KEY> from os.environ (e.g. JIRA_API_TOKEN). Composes with Vault Agent / op run -- wrappers without further changes; the only path that does.
  2. Tier 2 — OS keyring. macOS Keychain via /usr/bin/security (token via child stdin, never argv); Windows Credential Manager via in-process ctypes against advapi32. Linux falls through to Tier 3 in v1 — a libsecret backend is deferred to a v2 RFC.
  3. Tier 3 — dotfile. ~/.agentbundle/credentials.env, mode 0600 on POSIX, DACL-verified via icacls on Windows. The fallback floor.

Changing the order, or adding a new tier, is an Ask first action in the spec’s Boundaries section — the corporate-network constraints that justified the precedence are non-obvious.

Credentialed-CLI-class primitives must refuse the value-shaped flags --token, --api-token, --api-key, --bearer, --pat, --password. The CLI verb’s setup subparser registers these as tombstone arguments whose action emits the verbatim sentinel tokens cannot be passed via argv and exits non-zero; the agentbundle catalogue lint (_PackRules._check_credentialed_skills) refuses any primitive’s script that declares one of the banned names in an argparse.ArgumentParser.add_argument call. MCP-server-class primitives may accept header-naming flags (--bearer-header, --auth-header, --header-prefix) because those name which header to consult per-request, not the value.

Five anti-patterns rejected by name:

  • Tokens in skill argv — defeats the architecture rule.
  • A get verb that returns a cleartext token — any verb that prints the resolved token to stdout enables capture from a skill body. The credential-setup skill writes; a consumer’s check verb reads (resolves and returns 0/non-0 only); no skill or shim path returns the cleartext token to a caller other than the in-process credentialed primitive that owns the API call.
  • Per-skill dotfiles — the contract mandates one well-known per-user file; per-skill files multiply the wipe-on-rotation scope.
  • SSL_VERIFY=false defaults — --insecure is opt-in only and must emit a stderr warning.
  • Vendored copies of third-party API skills — pin upstream and audit; do not fork to silence a vendor’s lint.

Credentialed primitives ship from this catalogue running on corporate laptops; the network they live on imposes constraints the primitive must respect:

  • Honor HTTPS_PROXY / NO_PROXY from the environment. No hard-coded requests.get(...) without proxy resolution.
  • Honor the system trust store via REQUESTS_CA_BUNDLE, SSL_CERT_FILE, SSL_CERT_DIR. Corporate MITM CAs land here; ignoring them turns into a “works on the engineer’s laptop only” bug.
  • Refuse --insecure / verify=False as a default. Opt-in flag only; primitive emits a stderr warning whenever it fires.