Skip to content
Back to skills

How To

ASecurity

**Use this when:** your skill calls an external authenticated service (API token, vendor CLI, or corporate SSO) and the credential must never pass through the model. **Prerequisites:** 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. **Result:** a scaffolded, lint-passing credentialed skill directory with the correct broker wired, a `### Security rules (non-negotiable)` block in `SKIL...

  • 24 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 3, 2026
ai-agentspythonrustgoshellbashawsazuretestinggitapi

Works with

  • cli
  • api
  • mcp

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 6, 2026

npx -y skills add eugenelim/agent-ready-repo --skill how-to --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of How To?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for How To
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/eugenelim-how-to-0bb8fc52/badge)](https://www.skillsdirectory.com/skills/eugenelim-how-to-0bb8fc52)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
title: "How to add a credentialed skill"
summary: "Build a lint-clean authenticated skill that uses the correct broker and keeps credentials outside the model boundary."
pack: credential-brokers
kind: how-to
---

# How to add a credentialed skill

**Use this when:** your skill calls an external authenticated service (API token, vendor CLI, or corporate SSO) and the credential must never pass through the model.
**Prerequisites:** 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.
**Result:** 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/`](../../../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.

:::note
**When to use this** — your skill calls an external service that takes API tokens, Bearer auth, or session cookies via corporate SSO. If your skill only shells out to a binary the user has already authenticated on PATH (`gh`, `git`, `kubectl`) and the vendor binary owns the credential end-to-end, the `auth: cli` broker fits; everything else picks a different broker below.
:::

## 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-brokers` user-scope pack installed (`agentbundle install --pack credential-brokers --scope user`), if you're using the `creds` or `sso-cookie` broker.

## Step 1 — 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 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](#step-7--embed-the-security-rules-block-in-skillmd), one per broker; copy the one matching your choice.

## Step 2 — 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).

## Step 3 — Scaffold the skill directory

```
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/`).

## Step 4 — Declare the frontmatter

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

```yaml
---
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).

`tools/lint-agent-artifacts.py` refuses unknown `auth:` values; `metadata.credentialed: true` requires `metadata.auth`.

## Step 5 — Declare the schema (`auth: creds` and `auth: env` only)

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

```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.

## Step 6 — Import the broker in `scripts/cli.py`

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

```python
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:

```python
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.

## Step 7 — 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`:**

```markdown
### 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`:**

```markdown
### 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`:**

```markdown
### 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`:**

```markdown
### 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.
```

## Step 8 — 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).

```markdown
### 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`). Surface 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 — surface 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`; surface it.
- **Upstream 5xx or rate limit.** Surface 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.

:::note
The agent installs deps unattended (no secret) but never types the token. This is the credentialed-skill form of "self-bootstrapping": the non-secret setup is automatic; the secret stays a user gesture.
:::

## Step 9 — Declare the `credbroker` dependency (`auth: creds` only)

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

```
credbroker
```

```bash
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

`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.

### 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)

`credbroker` does **not** require PyPI. The [`release-credbroker`](../../../.github/workflows/release-credbroker.yml) 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):

  ```bash
  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`**:

  ```bash
  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.

## Step 10 — Set up the credential

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:

  ```bash
  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`](../../../docs/architecture/credentials.md#the-sso-cookie-broker).

## Step 11 — Run the lint

```bash
python3 tools/lint-agent-artifacts.py     # frontmatter schema
bash tools/lint-credentialed-skills.sh    # credentialed-skill rules
```

`lint-agent-artifacts.py` 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.

## Common pitfalls

- **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.

## Reference

- Reference consumer (runnable, shipped): [`packs/atlassian/.apm/skills/jira/`](../../../packs/atlassian/.apm/skills/jira) — a live `auth: creds` credentialed CLI
- Explanation: [`guides/credential-brokers/explanation/credentialed-skills.md`](../explanation/credentialed-skills.md)
- Related how-to: [How to author a skill](../../_shared/how-to/author-a-skill.md) — the general skill-authoring standards (structure, cross-platform scripts, the three-tier dependency policy); the `auth: cli` broker is where credential and tool-presence concerns meet.

## 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

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

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

```yaml
---
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](https://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

`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
canonical `Path.home() / ".agentbundle" / "bin" / "sso-broker.py"` path; `auth:
cli` falls through to broker-agnostic checks only).

### Three storage tiers

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.

### 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

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
  surface 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 surface.
- **`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.

### 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_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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…