Giving AI coding agents a public URL: the master-key delegation pattern
AI coding agents — Claude Code, Cursor, Aider, Devin — increasingly need to expose the work they produce on localhost. The feature looks innocuous: an agent runs your code, opens port 3000, surfaces a URL you can click in chat. The plumbing under it is asking a different question, one we've answered before in other domains: how do you delegate capabilities to a non-human caller that needs to act on your behalf without holding your full-access credentials? This post is about that question, the precedent answers (Stripe, OpenAI, AWS, GitHub), and the shape any tunneling service should adopt for AI agents in 2026.
TL;DR
The problem isn't a tunnel problem — it's a delegation problem. Don't paste your full-access tunneling token into the AI agent's environment. Instead: mint a master key that can mint scoped, short-lived child keys but cannot directly access tunnels itself. The agent reads the master key from its env, calls a keys create --ttl 1h endpoint to get a scoped child key for the tunnel it needs, runs that tunnel, and the child key expires on its own. If anything goes wrong, cascade-revoke the master key — every child key it ever minted dies in one operation. Same shape as Stripe restricted keys, OpenAI service accounts, GitHub fine-grained PATs, AWS STS.
The shape of the problem
Concrete scenario. You ask Claude Code to spin up a Next.js app, wire a Stripe webhook for testing, and share the preview with your designer. The agent does this in three steps:
- Generates the app and runs
pnpm devon port 3000. - Starts a tunnel so Stripe (and your designer) can reach the dev server.
- Hands you back a URL.
Step 2 is where the design question lives. The agent needs to authenticate to your tunneling service. Three ways to give it credentials:
- Paste your personal API token. Works. The agent now has your full account permissions — create / delete every tunnel on your account, view all captured requests, inspect billing.
- Create a second account for the agent. Doesn't actually help — the agent still has full permissions on that second account, you've just split the blast radius without limiting it. Plus the per-account quotas now fight you.
- Mint a master key, let the agent mint scoped child keys. The agent gets a credential that can do exactly one thing — mint short-lived child keys for tunnels in a project namespace. Each child key has a TTL and a scope. The master key itself can't access existing tunnels.
Option 3 is the actual answer. It mirrors how every production system delegates to non-human callers — see the precedents section below.
What goes wrong with naive approaches
The "paste your token in the agent's env" approach is the natural starting point. It works on day one. The failures appear later:
- The agent logs your token. Most AI-agent transcripts get saved somewhere — a local history file, a session log, occasionally a shared conversation export. If the token is anywhere in the agent's command output, it might be captured in those logs. Rotating tokens by hand because of one careless paste is a chore.
- The agent has indefinite access. Until you rotate the token, the agent (or anyone with the token from that captured log) can keep doing things on your account. Tokens with no TTL accumulate.
- You can't revoke just the agent. If the agent leaked and you suspect compromise, your only response is "rotate the whole account token" — which also kicks out your laptop, your CI, your other tools that share that token. The revoke is correctly-scoped if and only if you bought a separate token per consumer ahead of time, which nobody does.
- The agent's privileges exceed the task's needs. The agent needed to create one short-lived tunnel for one project. The token can delete every tunnel on your account. The blast radius of any agent mistake (or any token leak) is far larger than the task warranted.
None of these are theoretical — they're the same failure modes that drove every other industry to scoped-keys-with-TTL a decade ago. AI-agent workflows just rediscover the problem from a different angle.
The master-key delegation pattern
The pattern has three primitives:
- Master key (
mtk_master_*). Long-lived, scoped to a single tenant. Can mint child keys. Cannot directly create / read / delete tunnels. The master key never has the privilege the agent actually uses — it has only the privilege to delegate. - Child key (
mtk_child_*). Short-lived (typical TTL: 15 minutes to 24 hours). Scoped to a specific project namespace within the tenant. Can create / use one tunnel at a time within that project. Expires on its own. - Cascade revoke. Revoking a master key immediately invalidates the master key plus every child key it ever minted. One operation. Bounded blast radius on suspected compromise.
Three properties fall out of this for free:
- Time-bounded blast radius. A leaked child key dies on its own at TTL.
- Scope-bounded blast radius. A leaked child key in project A can't touch project B.
- One-click compromise response. If you suspect the master key leaked, cascade-revoke. Every child key it ever minted is invalid within milliseconds.
Precedents (Stripe, OpenAI, AWS, GitHub)
The pattern isn't novel. Every production system that delegates to non-human callers does some shape of this. Reading the same idea in four different domains makes the abstraction concrete:
- Stripe restricted keys. Your account has a publishable key and a secret key. The secret key is god-mode. For non-trusted code paths (background jobs, third-party integrations), Stripe lets you mint restricted keys with per-resource ACLs — "read customers, write charges, no refunds". Same shape: parent key with full power can mint scoped subkeys.
- OpenAI service accounts. A team admin can create service accounts for non-human consumers — CI, automation, AI agents. Each service account gets its own API key with project-scoped permissions. The team admin key itself isn't shared with the consumers.
- GitHub fine-grained personal access tokens. Replaced GitHub's classic PATs precisely because the classic PATs were unscoped. A fine-grained PAT picks which repositories and which permissions it grants — and importantly, has an expiry date that's enforced.
- AWS STS AssumeRole. Long-lived IAM credentials don't go on EC2 instances or in Lambda functions; instead, those workloads call STS to get short-lived credentials that expire within hours. Compromised credentials self-clean.
- HashiCorp Vault dynamic secrets. Database credentials are minted on demand by Vault, delivered with a TTL, and self-destruct. The long-lived credential (Vault's root) never touches the consumer.
The pattern in every case: a long-lived high-privilege thing delegates to a short-lived low-privilege thing, and the long-lived thing has cascade revoke. Master keys in tunneling are just the tunneling-shaped version.
How 21tunnel ships this
Concretely, the primitives in our product. (The CLI binary is called mytunnel; replace with your tool of choice if you're reading this for the general pattern.)
From the dashboard or CLI, you create a project (one master key per project):
# Create a project for the AI agent's work
$ mytunnel projects create staging-preview
Project created: proj_8f3a2b
Master key (save this — shown once): mtk_master_a1b2c3d4e5f6... That master key goes in the agent's environment, not on your laptop. The agent (or any process holding the master key) can mint child keys scoped to staging-preview:
$ MTK_MASTER=mtk_master_a1b2c3d4... \
mytunnel eval mint --project staging-preview --ttl 1h --output-env
# Outputs shell exports the agent can source:
export MYTUNNEL_AUTH_TOKEN=mtk_child_x9y8z7...
export MYTUNNEL_PROJECT=staging-preview The agent now has a child key that:
- Can only operate within project
staging-preview(subdomain isolation). - Expires in 1 hour regardless of what the agent does with it.
- Cannot mint further child keys (no privilege escalation).
- Shows up in the audit log of the master key.
If you ever need to revoke everything the agent has done:
$ mytunnel keys revoke --cascade mtk_master_a1b2c3d4...
Revoked 1 master key.
Revoked 17 child keys (cascade). One operation. Bounded. Audit log preserved.
Concrete configs
How to wire this into the four most common agents in 2026. Each follows the same pattern: master key in env, agent runs mint, agent runs tunnel.
Claude Code
Claude Code reads environment variables from your shell and from .claude/settings.json. Easiest setup: put the master key in your shell rc or in .claude/settings.local.json (the local file is gitignored by default).
// .claude/settings.local.json
{
"env": {
"MTK_MASTER": "mtk_master_a1b2c3d4e5f6..."
}
} Then in your CLAUDE.md or in the conversation directly:
When you need to expose localhost:
1. Source env vars with:
eval "$(mytunnel eval mint --project staging-preview --ttl 1h --output-env)"
2. Run the tunnel:
mytunnel http 3000 --subdomain preview
3. Share the URL it prints with the user. Claude Code can now run the full flow autonomously without ever seeing your real account token.
Cursor
Cursor's agent mode reads from .env in the workspace. Same pattern:
# .env (gitignored)
MTK_MASTER=mtk_master_a1b2c3d4e5f6... Add a project doc that tells the agent what to do:
# .cursor/rules/tunneling.md
When the user asks to share a preview, run:
eval "$(mytunnel eval mint --project $PROJECT --ttl 1h --output-env)"
mytunnel http 3000
Do NOT use the MTK_MASTER token directly to run tunnels. Aider
Aider uses standard environment variables. The --env-file flag points to a dotenv file:
# .aider.env
MTK_MASTER=mtk_master_a1b2c3d4... $ aider --env-file .aider.env Inside the session, ask aider to run the mint command before the tunnel.
Devin
Devin's secrets panel (in the workspace settings) accepts named env vars. Add MTK_MASTER. Devin runs each task in an isolated VM so the secret never leaves that workspace.
Same flow as the others: mytunnel eval mint ... before any tunnel command.
Anti-patterns (things we deliberately did NOT build)
Symmetric to listing what we did build — here's what we decided not to. Each was rejected for a specific reason.
- An SDK for AI agents. We could ship
@21tunnel/agentas a Node / Python package. We chose not to. The CLImytunnelis the one interface — every agent calls it the same way. An SDK introduces a versioning surface to maintain, an installation step, and a confusion ("which interface should I use?"). The CLI works in every language because every language can shell out. - An MCP server for tunnel control. We could ship a Model Context Protocol server that Claude Desktop / Cursor / etc connect to natively. We chose not to (yet). It would couple us to MCP's release cycle. The CLI-via-bash invocation works on every agent regardless of MCP support. We'll revisit when MCP is a year more mature. (Note: the separate post on MCP server tunneling is about your MCP server, not ours — different problem.)
- A separate machine-key primitive. Some teams ask for "machine keys vs user keys vs agent keys" as three distinct primitives. We collapsed to two: user keys (full account access, what humans use) and master/child keys (the delegation primitive, what agents use). Three primitives would have been one too many for a category that's still 18 months old.
- IP-allowlisting on child keys. Tempting — bind the child key to the agent's source IP. We rejected it because most AI agents run in short-lived cloud VMs (Devin, Aider when wrapped in GitHub Actions) where the source IP is unpredictable. The TTL on the child key is the better defense.
- OAuth flow for agent auth. Some designs propose: the agent does an OAuth dance with the tunneling service, gets a delegated token. Too much human-in-loop friction for a workflow where the human is supposed to be hands-off. The master-key handoff is one-time-setup-then-fire-and-forget.
What every tunnel service should adopt
Not a sales pitch, a call-out: every tunneling product will need this primitive in 2026. ngrok, Cloudflare Tunnel, Pinggy, Tailscale Funnel, frp, bore — pick the shape that fits your architecture. Three properties to get right:
- The master key cannot directly act. If the master key can also create tunnels, it isn't a master key; it's a renamed user token. The privilege asymmetry is the whole point.
- Child keys must have enforced TTL. Not advisory ("expires_at" as a field nobody checks); enforced at the auth layer.
- Cascade revoke is one operation. Not "loop through child keys and revoke each"; one atomic call that nukes the entire tree. Either the database tracks parent-child explicitly (we use a
parent_key_idforeign key) or the capability tokens have a parent identifier in their payload that the auth path checks.
The 2024-2026 wave of AI-agent tooling is moving from "give the agent your password" (first instinct) to "give the agent a scoped session" (second instinct). We're building the third instinct: scoped sessions self-mint from a scoped delegator. Whatever tunneling product you ship, this is the shape developers will expect from you within 18 months. Better to ship it now and shape the category than rebuild it as a feature-flag scramble later.
Frequently asked questions
Why can't I just give my AI agent my ngrok auth token?
You can, and it works. The problems show up at the second-order: the token has all your permissions indefinitely, the agent might log it to a transcript that gets shared, and there's no scoped revoke — if you suspect leakage, you have to rotate your entire account. The master-key pattern fixes all three.
What is a master key in tunneling, exactly?
An API key that can mint child keys but cannot directly access tunnels itself. You give the master key to the AI agent. The agent uses it to mint short-lived scoped child keys for each tunnel it needs. Cascade revoke on the master key invalidates every child key it ever minted.
Does this work with Claude Code, Cursor, Aider, Devin?
Yes — see the concrete configs section above. The pattern is environment-variable based: master key in env, agent shells out to a mint command, gets back a scoped child key, runs the tunnel.
What is cascade revoke?
A single operation that invalidates a master key plus every child key that master ever minted. One click in the dashboard or one CLI command. Use it when you suspect any of the keys has leaked.
How is this different from a standard "API key with permissions" pattern?
It isn't, structurally. The pattern is identical in shape to Stripe restricted keys, OpenAI service accounts, GitHub fine-grained PATs, and AWS STS. The novelty is that tunneling services hadn't adopted it yet — tunneling as a category pre-dated AI-agent workflows by a decade, so the delegation primitive wasn't in the product DNA.
Can I use this pattern without 21tunnel?
Sure — the pattern is industry-general. Build it on top of ngrok's auth-tokens API (write your own keys mint service around it), or use Stripe-style restricted keys on a self-hosted frp install. It's more work than picking a product that ships it, but it's not novel cryptography.
Want to try the master-key flow in your AI-agent setup? 21tunnel ships it on every tier including the free Hobby plan. Start at /ai/ — the agent-friendly setup page or read the scored comparison of every ngrok alternative we wrote at the same time as this post.