Delegating credentials to sub-agents

When your agent spawns a worker — a VM, a sub-process, a CI runner — it can share a subset of its permissions without exposing its own identity.

When to delegate

  • Multi-agent workflows
    Parent agent orchestrates, sub-agents execute specific API calls.
  • Ephemeral VMs
    Spin up a worker VM that needs GitHub access for 2 hours, then auto-expires.
  • CI / CD pipelines
    Give your pipeline scoped API access without storing long-lived secrets.
  • Sandboxed execution
    Run untrusted code with minimal permissions — it can't escalate beyond its delegate scope.

Create a scoped delegate

A scoped delegate gets a fixed subset of the parent's approved scopes. No dynamic expansion — what you give is what it gets.

$ keychains delegates add pr_a1b2c3 --scopes "github::repo" --ttl 3600
> Delegate created: del_x7y8z9
> Mode: scoped
> Scopes: github::repo
> Expires in: 1 hour
> Ship KEYCHAINS_DELEGATE_ID and KEYCHAINS_DELEGATE_KEY to the remote system

Create a wildcard delegate

A wildcard delegate starts with zero scopes and accumulates them as the user approves. The parent's scopes act as a hard ceiling.

$ keychains delegates add pr_a1b2c3 --wildcard --ttl 7200
> Delegate created: del_w1c2a3
> Mode: wildcard
> Scopes: (none — approved on demand)
> Parent ceiling: github::repo, google::gmail.readonly

Note: The wildcard delegate can only request scopes the parent has already approved. Requesting a scope outside the parent's set returns a hard scope_refused error — no approval URL.

The two-tier scope check

When a wildcard delegate requests a scope, two checks happen:

  • Tier 1 — Parent Ceiling
    Is the scope in the parent's approvedScopes? If no: hard refuse. No approval URL.
  • Tier 2 — Delegate Approval
    Scope is in parent but not the delegate? Ask user — returns 403 with approval URL.

Using delegates from code

import { mintDelegateToken, proxyFetch } from '@keychains.dev/machine-sdk';
// On the remote system — env vars shipped from parent
const { token } = await mintDelegateToken(
  process.env.KEYCHAINS_DELEGATE_ID,
  process.env.KEYCHAINS_DELEGATE_KEY
);
const res = await proxyFetch('https://api.github.com/user/repos', { token });

Warning: Delegate private keys are secrets. Always use short TTLs and revoke delegates as soon as the task completes.

Revoking a delegate

Revoke from the CLI, SDK, or dashboard. Takes effect immediately.

$ keychains delegates rm del_x7y8z9
> Delegate del_x7y8z9 revoked

Tip: Revoking the parent permission automatically revokes all its delegates via cascade revocation.

Delegation mode matrix

Parent Delegate Behavior
scoped scoped Fixed subset. No expansion.
scoped wildcard Accumulates up to parent's fixed set.
wildcard scoped Fixed subset of parent's dynamic pool.
wildcard wildcard Dynamic accumulation. Parent is hard ceiling.