- Docs
- Build a Catalogue
- Author a skill
- Reference
- Credentialed skill walkthrough
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.
Before you start
Section titled “Before you start”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-brokersuser-scope pack installed (agentbundle install --pack credential-brokers --scope user), if you’re using thecredsorsso-cookiebroker.
Pick a broker
Section titled “Pick a 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 thecredbrokerlibrary (pip install credbroker), imported in-process — declare it in your skill’srequirements.txt(Step 9). Pick this for static API tokens / PATs.sso-cookie— session cookie acquired via a browser SSO flow. Your skill importscredbrokerand callsload_sso_cookies/refresh_sso_session; the library subprocess-invokes thesso-broker.pyengine 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 viasubprocess.run([sys.executable, "scripts/cli.py", ...]). The argv ban applies (no--token/--api-token/--bearer/--pat/--passwordflags) 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).
Scaffold the skill directory
Section titled “Scaffold the skill directory”your-skill-name/├── SKILL.md├── scripts/│ └── cli.py└── references/ └── creds-schema.toml # only for auth: creds / auth: envPlace this under your pack’s .apm/skills/ directory (e.g. packs/<your-pack>/.apm/skills/your-skill-name/).
Declare the frontmatter
Section titled “Declare the frontmatter”The frontmatter shape varies by broker. For auth: creds:
---name: your-skill-namedescription: <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 = falsesecret = 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.
Import the broker in scripts/cli.py
Section titled “Import the broker in scripts/cli.py”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 guardjar_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 — itnames the cause more reliably than the exit code, whose meaning variesbetween 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 thecredential-setupskill (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 nextget-cookiesopens 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:
credbrokerpython -m pip install -r requirements.txtThe 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:
- Vendored floor (zero-pip, always present at user scope). When a user installs the
credential-brokerspack 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/libtosys.pathat lowest precedence (the five API CLIs inside their__package__bootstrap;credential-setup’ssetup.pyahead of its top-level import). So a no-repo user-scope install resolvesimport 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). - Offline / local pip (corporate, no PyPI). A
pip installof the wheel (from an internal index or a local.whl) landscredbrokerin site-packages, which sits earlier onsys.paththan the floor — so it wins over the floor and unlocks the[crypto]vault. No PyPI dependency. Detailed just below. - PyPI (open adopters).
pip install credbroker[crypto]from public PyPI; same site-packages precedence as layer 2. Published ascredbroker 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.
Package independence from AgentBundle
Section titled “Package independence from AgentBundle”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.
Installing without PyPI (corporate)
Section titled “Installing without PyPI (corporate)”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/simplepython -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.whlpip 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.
Set up the credential
Section titled “Set up the credential”Once everything is in place, populate the credential for your namespace:
-
auth: creds— invoke thecredential-setupskill (ships with thecredential-brokerspack). It reads yourcreds-schema.toml, prompts for each required key (secret keys viagetpass; non-secret viainput), 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 callscredbroker.register_sso_session(the jira skill’s ispython 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.
Run the lint
Section titled “Run the lint”agentbundle catalogue verify --root . # frontmatter schemabash tools/lint-credentialed-skills.sh # credentialed-skill rulesagentbundle 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 ifscripts/imports no credential resolver (from credbroker import …, or the legacyfrom .credentials_shim).auth: env— refuse if any declared<NAMESPACE>_<KEY>is never read inscripts/.auth: sso-cookie— refuse ifscripts/does not reach the broker throughcredbroker; 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.
Common pitfalls
Section titled “Common pitfalls”- Printing
creds.API_TOKENinside a debugprint(...). The token reaches stdout where any caller can capture it. Uselen(creds.API_TOKEN)only if you must prove resolution, and ideally don’t even disclose the length. - Forgetting to declare
credbrokerinrequirements.txt. Thefrom credbroker import …resolver import fails withModuleNotFoundError: credbrokeruntil 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" / …plussubprocess.run(...). Callcredbroker, 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
--tokenflag “just for local testing”. The argv ban applies in every environment and every broker; thecredential-setupskill is the supported escape hatch for thecredsbroker.
Reference
Section titled “Reference”- Reference consumer (runnable, shipped):
packs/atlassian/.apm/skills/jira/— a liveauth: credscredentialed CLI - Explanation:
guides/credential-brokers/explanation/credentialed-skills.md - Related how-to: How to author a skill — the general skill-authoring standards (structure, cross-platform scripts, the three-tier dependency policy); the
auth: clibroker is where credential and tool-presence concerns meet.
The credentialed-skill contract
Section titled “The credentialed-skill contract”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.
Two-layer architecture
Section titled “Two-layer architecture”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.
Frontmatter declarations
Section titled “Frontmatter declarations”A credentialed skill declares three project-specific flags under the
metadata: block of its SKILL.md frontmatter:
---name: your-skill-namedescription: <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.
Four brokers — pick one per skill
Section titled “Four brokers — pick one per skill”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 thecredbrokerlibrary (pip install credbroker), imported in-process; the build-projectedcredentials_shimit replaced is retired forcredsconsumers (the four-broker taxonomy above is unchanged).sso-cookie— session cookie acquired via a headed-browser SSO flow. The skill resolves the session through thecredbrokerSSO resolver (from credbroker import load_sso_cookies), which subprocess-invokes~/.agentbundle/bin/sso-broker.py(projected by thecredential-brokerspack at user scope) — mirroring howcredsmoved broker resolution intocredbroker. A skill that still resolves the broker in its ownscripts/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).
Three storage tiers
Section titled “Three storage tiers”Credentials resolve in this order, first-hit-wins per key:
- Tier 1 — env var.
<NAMESPACE>_<KEY>fromos.environ(e.g.JIRA_API_TOKEN). Composes with Vault Agent /op run --wrappers without further changes; the only path that does. - Tier 2 — OS keyring. macOS Keychain via
/usr/bin/security(token via child stdin, never argv); Windows Credential Manager via in-processctypesagainstadvapi32. Linux falls through to Tier 3 in v1 — alibsecretbackend is deferred to a v2 RFC. - Tier 3 — dotfile.
~/.agentbundle/credentials.env, mode0600on POSIX, DACL-verified viaicaclson 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.
The argv ban
Section titled “The argv ban”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.
Anti-pattern register
Section titled “Anti-pattern register”Five anti-patterns rejected by name:
- Tokens in skill argv — defeats the architecture rule.
- A
getverb that returns a cleartext token — any verb that prints the resolved token to stdout enables capture from a skill body. Thecredential-setupskill writes; a consumer’scheckverb 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=falsedefaults —--insecureis 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.
Corporate-network requirements
Section titled “Corporate-network requirements”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_PROXYfrom the environment. No hard-codedrequests.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=Falseas a default. Opt-in flag only; primitive emits a stderr warning whenever it fires.