thundeeran.devEssay 01 · agents · securityHomeAll essays
Engineering · Agent security

Guardrailing agents by persona

When an agent can write code and hit real APIs, the question stops being "can it do the task" and becomes "what is it allowed to touch." A design for scoping agents by the person who drives them — and defending in depth around that.

T
By Thundeeran · August 2026 · ~9 min read
Run the reference implementation See it in the essay ↓
TL;DR — the 40-second version
In this article Core ideaRoles & agentsMethodEnforcementMCP layerDefense in depthTakeawayIn practice

The moment you give an agent write access to a repository, a cloud account, or a ticketing system, its capabilities stop being a demo and start being a liability. A model that can open a pull request can also, in principle, force-push to main, paste a secret into a commit message, or deploy to production at 2am because a tool description said it could. The interesting engineering problem was never the reasoning. It's the blast radius.

This is how I think about that: organize guardrails around the user driving the agent, enforce them outside the model, and layer defense so that no single failure is catastrophic.

01 — the core ideaThe persona is the user

The cleanest starting move is to stop treating a "persona" as a bespoke agent configuration and start treating it as the human being who drives it. The agent authenticates as that user, inherits their existing organizational role and permissions as a hard ceiling, and can never exceed what the person is themselves allowed to do.

Don't invent a permission system for agents. Reuse the one that already governs the humans.

That single decision buys three properties for free. There's no parallel permission system — agent scope derives from the RBAC that already governs the human, so a PM's agent cannot write code because the PM's own credentials can't. Accountability is clear — every action traces to the person it acted as. And deny-by-default falls out automatically: anything the user can't do, the agent can't either.

Each persona then collapses to two questions: what can this user do in the org (which sets inherited scope), and how well can they verify the agent's output (which sets required rigor).

Takeaway

Reuse the org's existing RBAC — don't invent a second permission system for agents.

02 — the mapFour roles, two agents

For a team shipping software, the users worth modeling are the individual contributor, the tech lead, the product manager, and the engineering manager — each driving a coding agent (writes code, opens PRs) or a planning agent (reads context, produces plans and tickets), or both. Laid against those two agents, the scope-and-rigor picture looks like this:

Coding agentPlanning agent
IC engineer Primary · low
Write to own branches, open PRs. No merge or deploy. Reviews every diff.
Secondary · low
Decompose own ticket into steps. Read code + tickets. Informal.
Tech lead Occasional · low
Same write scope as IC. Leverage is in planning, not code.
Primary · medium
Read the whole codebase, sequence work, weigh trade-offs.
Product manager Not granted
No code write access. Read-only status at most.
Primary · high
Produce scoped tickets. Can't verify feasibility — tech-lead gate.
Eng manager Oversight · medium
Read code, PRs, metrics. Approve. Delegates writes.
Oversight · medium
Read plans, status, risk. No production writes.
Scope inherited from the role · rigor set by verification capacity

Two things this surfaces. The PM + coding agent cell is deliberately empty — the org role has no code-write authority, so neither does its agent, no special-casing required. And the rigor gradient runs opposite to technical ability: the IC's coding agent is the loosest because they catch errors, while the PM's planning agent is the tightest because they can't. Rigor tracks who can verify, not who is senior.

Takeaway

Rigor tracks who can verify the output, not who's senior.

03 — the methodBuilding the guardrails

The same four-step procedure runs for every persona; only the inputs change.

Takeaway

Inherited identity sets the ceiling; least privilege sets the grant.

04 — enforcementWhere the limit actually bites

Here's the part that has to be right: a limit that lives in the prompt is not a limit. "Only use this for staging" written into a tool description is persuasion — the model can ignore it. Real enforcement lives in the path between the agent and your systems. A request runs as the user down a pipeline, and any stage can stop it. Pick a role and an action below and watch where it lands.

Role
Action
Executed Gated — held for a human Denied — role lacks permission Filtered — never offered
Interactive · the agent runs as the user the whole way down

The two purple stages do double duty. The broker checks each action against the user's role; the scoped credential is a second, independent ceiling — a token pre-narrowed to what the user may touch. So even if the policy check were bypassed, the credential still can't exceed the role. The agent can want to exceed its role; it just structurally can't.

Takeaway

A limit that lives in the prompt is not a limit — enforce it in the broker.

05 — the mechanismDoing it with MCP

Concretely, this is where MCP earns its keep. For most agents the connected MCP servers are the only door to the outside world — the agent can't hit an endpoint that isn't exposed as a tool. So "limit access to endpoints" becomes "control what the MCP layer exposes and enforces." The strongest shape is a proxy MCP: the agent connects only to it, holds no real credentials, and every call descends a stack of checks before reaching a real server.

Persona profile allowlist · scopes · policy configures Agent persona-scoped sees filtered tools tools/call tools/list Proxy MCP · broker Tool filterexposes allowed subset hide Policy checkrole + argument rules deny Human gateirreversible calls hold Credential vaultinjects scoped token scope Audit log — every call recorded GitHub MCPread · create_pr Jira MCPticket tools Cloud MCPdeploy: staging scoped call GitHub API Jira API Cloud API upstream MCP servers real systems
The broker, in MCP terms · tool allowlist and credential scope are two independent ceilings

Two cautions that are specific to MCP, because they're where teams get burned. Tool descriptions are not a security boundary — every real limit has to run in the server or proxy code, never in the natural language the model reads. And tool results are an injection surface — a fetched page or a ticket body can carry instructions trying to get the agent to call something dangerous, the classic confused-deputy path. Combine untrusted results with tight allowlists and human gates so a poisoned result can't reach a destructive endpoint.

Takeaway

Tool descriptions aren't security. Tool results are an injection surface.

06 — defense in depthBeyond the MCP ring

MCP guards the agent's hands — what it can touch. But that's one ring. A call reaching an allowed endpoint isn't the only failure mode. Around MCP sit the layers that guard what the agent reads, emits, and runs, and how far it can go.

Agent + judge 1 2 3 4 5
Agent + independent judge outside MCP
A separate supervisor reviews each proposed action and can veto it — persuasion can't turn off a judge it doesn't control.
1
I/O guards outside MCP
Injection & PII screening on inputs and untrusted tool results; secret-leak checks on output.
2
MCP proxy the MCP layer
Tool & endpoint scope — the hands. Covered above; one ring among many.
3
Sandbox + egress outside MCP
Isolated, ephemeral runtime with a network allowlist. Contains blast radius; blocks exfiltration.
4
Control loop outside MCP
Step caps, budgets, and a kill switch that halts on anomalous behavior.
5
Observability outside MCP
Traces and audit over everything — the detective layer that tunes the rest.
Any single ring can fail and the others still hold

The one that people forget is the sandbox. MCP scoping is moot if the agent can curl an arbitrary host from its shell — so the environment ring matters as much as the endpoint ring. For a coding agent, the two highest-leverage layers outside MCP are the sandbox with egress control (contains the damage) and the independent judge (catches what static policy can't express).

Takeaway

MCP is one ring — the sandbox and the judge catch what it can't.

07 — the takeawayWhat it comes down to

Strip it back, and the whole design rests on four ideas:

1

Hard guardrails enforce the ceiling. Identity, scope, and gates run in code — not persuasion.

2

Soft guardrails shape behavior underneath. Persona and self-checks make the agent want to stay in bounds.

3

Rigor tracks verification capacity. How much you need is set by whether the human can catch the mistakes.

4

Boundaries are hand-offs, not walls. At its edge, a good agent escalates rather than refuses.

The agent can reason its way toward anything. The job is to make sure that when it does, the only things it can actually reach are the ones the person behind it was always allowed to touch.

08 — in practiceA reference implementation

None of this is abstract. The same design drops into GitHub Copilot CLI as a guardrail proxy: Copilot launches it as its GitHub MCP server, and every call is filtered, checked, credentialed, and logged before it reaches the real github-mcp-server. Copilot never holds the token. The whole thing is open source — Node built-ins only, and it runs offline against a mock upstream, so you can clone it and watch the policy tests pass without a Docker daemon or a GitHub token.

thundeeran/labs / guardrailing-agents Node, no deps · runs offline View source on GitHub →
# clone and run the tests — no Docker or token needed
git clone https://github.com/thundeeran/labs
cd labs/guardrailing-agents && npm test
The persona is the user The agent inherits their role as a hard ceiling — and can never exceed what that person is allowed to do. The user IC engineer a role from your org's RBAC runs as Agent Copilot CLI holds no token of its own stdio Proxy · broker enforces the user's role as a ceiling scoped token narrowed to this user GitHub via github-mcp-server the real API As that user, the proxy clears every call through five checks: 1 Filter tools/list expose only the tools the user's role allows 2 Check every call role · repo · branch · human gate 3 Mint a scoped token short-lived (JIT), narrowed to what the user may do 4 Rotate before expiry re-mint · respawn · re-handshake the upstream 5 Audit every call one structured line per decision Everything downstream is scoped to the person — so even a policy bug can't let the agent exceed their role.
The reference implementation, end to end · the same diagram ships in the repo README
// ~/.copilot/mcp-config.json — Copilot connects only to the proxy
{
  "mcpServers": {
    "github-mcp-server": {          // this name replaces Copilot's built-in server
      "command": "node",
      "args": ["/path/to/proxy.mjs"],
      "env": {
        "GUARDRAIL_PERSONA": "ic-engineer",
        "GUARDRAIL_TOKEN_SOURCE": "command",
        "GUARDRAIL_TOKEN_CMD": "/path/to/mint-github-token.sh"  // mints a short-lived token, JIT
      }
    }
  }
}

proxy.mjs — a newline-delimited JSON-RPC stdio broker, no dependencies

No token lives in the config. A long-lived secret in a config file doesn't scale and is a leak waiting to happen. Instead the proxy mints a short-lived, scoped token just-in-time — a GitHub App installation token (~1h TTL, scoped to specific repos and permissions) — injects it into the upstream only, and rotates it before it expires. Nothing durable to steal, nothing to rotate by hand.

The proxy enforces the persona policy verbatim. For an IC engineer it filters merge_pull_request out of the tool list, denies writes to main, and blocks any repo outside the allowed set. For a PM it exposes read-only tools and nothing else. For an eng manager it holds merge_pull_request for out-of-band human approval. Every decision lands in an audit line.

end-to-end policy tests: 12 passed, 0 failed

One caveat the code makes obvious: the proxy only guards MCP. Copilot can still run shell commands, so you pair it with narrow --deny-tool shell permissions and a sandbox with egress control — otherwise the agent could curl GitHub directly and walk around the whole thing.

Credentials, just in time

The minter is any command that returns a short-lived, scoped token for the persona. The proxy runs it at session start and again before expiry:

# GUARDRAIL_TOKEN_CMD <persona>  →  prints JSON on stdout
$ mint-github-token.sh ic-engineer
{ "token": "ghs_...", "expires_at": "2026-08-01T12:00:00Z" }

A GitHub App installation token is the natural source — scoped to specific repos and permissions, ~1h TTL. When it nears expiry the proxy re-mints, respawns the upstream, and replays the MCP handshake, so a long session never runs on a stale or over-broad token — and there is no durable secret to leak.

rotation test: token re-minted mid-session, calls pass before and after

One proxy, many personas

Personas are session-scoped — one per Copilot session. Each gets its own isolated config and its own scoped token; you switch between them rather than running them side by side. Register two GitHub servers at once and the agent just reaches for the more permissive one.

# no long-lived token anywhere — each persona mints a short-lived one JIT
bash copilot-as.sh ic-engineer        # isolated COPILOT_HOME, guarded session
bash copilot-as.sh pm                 # a separate, read-only session

Every profile is the same proxy with just one value swapped — the persona — and no token in the file:

// ~/.copilot-profiles/<persona>/mcp-config.json
{ "mcpServers": { "github-mcp-server": {
  "command": "node", "args": ["/path/to/proxy.mjs"],
  "env": {
    "GUARDRAIL_PERSONA": "ic-engineer",                 // ← the only thing that differs
    "GUARDRAIL_TOKEN_SOURCE": "command",
    "GUARDRAIL_TOKEN_CMD": "/path/mint-github-token.sh"  // ← JIT, scoped per persona
  } } } }

copilot-as.sh writes this per-persona under ~/.copilot-profiles/ and launches with COPILOT_HOME + --disable-builtin-mcps

References

T

Thundeeran

Writing about agents, security, and the systems around them at thundeeran.dev. This piece grew out of a design for scoping coding and planning agents by the person who drives them.

Clone & run the code