Skip to content
induwara.lk
Premium
Opinionai-agentssecurityopen-source

Pigeon: a signed pass for what your sub-agent may do

Pigeon issues a narrowed, signed credential to AI sub-agents instead of copying your API key. I read the spec and the security notes — here is what it actually buys a small team.

Induwara Ashinsana5 min read
GitHub repository page for Pigeon showing Python code granting and verifying an agent authority
Image: GitHub

Almost every AI sub-agent permissions bug I have seen starts the same way: the parent agent spawns a child and hands it the same API key. The child now has everything the parent had. Deploy to production. Read the payments table. Merge to main.

Pigeon (pigeonlabsHQ/pigeon on GitHub) is a small Python library that attacks exactly this. Instead of copying the key, you mint the child a Pigeon Pass: a signed credential describing what it may do, and nothing wider.


🔑 The real failure is key copying, not model misbehaviour

Most of the agent-safety conversation is about the model: will it hallucinate, will it get prompt-injected, will it do something silly. That is the wrong layer to fix first. The thing that turns a silly action into an incident is that the silly actor was holding a key with full scope.

Pigeon's framing is blunt and I think it is right:

Identity tells you who the agent is. Authority tells you what it may do.

We have spent years getting identity right for humans and then handed our agents a shared secret with no scope at all. If you have ever stuffed a scope claim into a token and hoped the downstream service checked it, this will feel familiar — you can decode a JWT here and see how little most of them actually constrain.


📋 What is actually on a Pass

A Pass is not a profile of JWT, macaroons, Biscuit, or UCAN. The spec says so explicitly. It is its own format, signed with Ed25519, and every field is mandatory — unknown or missing fields are malformed, not ignored.

The permission model is three-dimensional:

Dimension Example Child may
capabilities ["deploy", "open_pr"] only subset, exact match, no wildcards
resources ["environment:staging", "repo:acme/api"] only narrower patterns; trailing * allowed, * alone is root-only
constraints {"max_deploys_per_hour": 3} keep every parent dimension; may add more

The core invariant is one sentence: a child must never carry more effective authority than its parent. Try it anyway and you get a DelegationError with reason_code == "PRIVILEGE_ESCALATION".

from pigeon import delegate, grant, verify

parent = grant(
    subject="agent:orchestrator",
    capabilities=["deploy", "open_pr"],
    resources=["environment:staging", "repo:acme/api"],
)

worker = delegate(parent, subject="agent:pr-bot",
                  capabilities=["open_pr"], resources=["repo:acme/api"])

denied = verify(worker, action="deploy", resource="environment:staging")
assert denied.reason_code == "CAPABILITY_NOT_GRANTED"

The detail I like most: verify never returns a bare boolean. A denial carries a reason code, a message, and the requested vs allowed comparison that failed. That is the difference between a library you can debug at 2am and one you rip out.


⚠️ The security file is the reason I trust it

Most agent-security projects oversell. Pigeon's SECURITY.md does the opposite, and it is the strongest signal in the whole repo.

Pigeon does Pigeon does not
Fails closed when narrowing cannot be proven Stop prompt injection
Verifies the whole chain, not just the leaf Enforce a dimension you did not write
Invalidates the signature on any tampered field See revocations issued after an offline Pass was minted
Counts rate and count against every ancestor Manage, rotate, or recover your keys

Three admissions stand out.

  1. Same-process crypto is nearly theatre. If the issuer and verifier are the same process, the signature adds little over a plain data-structure check. It earns its keep when the Pass crosses a process, machine, or organisation.
  2. v0.1 ships no durable store. In-memory replay, revocation, and usage stores vanish on restart, so rate and count budgets reset. That is more permissive than most people would assume.
  3. The recommended default TTL is one hour, because short expiry is the only revocation an offline verifier really has.

Key takeaway: the enforcement point is the whole product. As the README puts it, "If the runner never calls verify, the Pass is decoration." Signing changes nothing if the tool runs regardless of the answer.


🛠️ Why this matters if you are building on a free tier

Here is the part that makes it relevant for a solo developer or a three-person team in Colombo rather than a security team at a bank.

  • There is no server. No control plane to host, no per-seat pricing, no vendor. You change two places in code you already wrote: the spawn site and the tool site.
  • It is MIT-licensed Python 3.12+, installed with git clone and pip install .. Total cost: nothing.
  • It forces you to write the policy down. Most of us have never actually enumerated what our automation is allowed to touch. Filling in capabilities, resources, and constraints is uncomfortable in a productive way.

That third point is the real value, and it survives even if you never ship Pigeon. The exercise of listing what a sub-agent may do is worth an afternoon on its own. I run an autonomous build pipeline on this site, and the honest answer to "what may the build stage touch?" was, for a long time, "whatever the process could reach."

There is also an MCP middleware helper: the client mints a narrower Pass per tool call, the server verifies before the handler runs. The repo is careful to say this is an enforcement point and not part of the MCP specification. Given how many people are now wiring MCP servers into agents without any per-tool boundary, that is a pattern worth copying by hand even if you skip the library.


🤔 Where I would push back

Two honest reservations.

  • v0.1 means v0.1. Nine constraint ops, no persistent store, and a protocol the author invites people to find escalation bugs in. I would not put it in front of a payments flow this month.
  • An omitted dimension is not enforced. If you did not put environment:production out of reach, production is in reach. The protocol will not invent a policy you did not sign. Your Pass is exactly as good as your imagination about what could go wrong, which is a familiar and uncomfortable property of every allowlist ever written.

What this means for you

If you are running agents that spawn other agents, do this today regardless of whether you adopt Pigeon:

  1. Stop passing the parent key down. Keep the real secret on the runner.
  2. Write the allowed action list somewhere machine-readable, even if it starts as a dict.
  3. Put the check where the side effect happens, not where the agent is spawned. That is the only place it counts.
  4. Set an expiry in hours, not weeks, on anything you do mint.
  5. Make denials explain themselves — reason code plus requested-vs-allowed, or you will disable the check the first time it blocks you unfairly.

Pigeon calls itself a small primitive, not a platform. That modesty is the point. The idea it encodes, that authority should narrow every time it is delegated, is older than AI agents and does not need a library to be useful. But having it in twenty lines of Python, MIT-licensed and serverless, removes the last excuse for not doing it.

#ai-agents#security#open-source
IA

Induwara Ashinsana

Information Systems student at UCSC and Executive Director at Ryzera Technologies. Writes about software, AI, and what it means for builders in Sri Lanka.

About the author →

Keep reading