Agent firewall

Your AI agent is one poisoned input from being the attacker.

A single crafted ticket, web page, or file can turn an agent against the very access you gave it. Grenz is a local firewall built for that moment. It refuses every action you did not explicitly grant, keeps your real credentials out of the agent's reach, scopes every sub-agent strictly down from its parent, and can hand out decoy credentials that revoke the agent the instant one is touched. Anything you did not think to allow is denied by default — not flagged after the fact.

agent: claude-codepolicy: github-safe-defaults
Read a repositorygithub repo:read
allow
Comment on a pull requestgithub pr:comment
allow
Merge a pull requestgithub pr:merge · waiting on a human
approval
Delete a repositorygithub repo:delete · not granted
deny
Decided locally, in the request path, before anything reaches GitHub.
MIT licensedSingle binary, no runtime deps122 test filesNo network call on the decision pathGitHub · Linear · Slack · MCP

The problem

An agent gets a badge, or it gets the master key. Today there is only the master key.

When you hire a contractor you don't hand over the key to every door in the building. You give them a badge: these rooms, these hours, and it stops working when the job ends. Software has had that for people for decades. It does not have it for agents — so agents get a person's credentials, not because anyone decided that was wise, but because nothing else exists to hand over.

A person's credential

What an agent holds today

  • Everything that person can reach — every repository, record, and channel
  • No distinction between reading something and destroying it
  • Works indefinitely, from anywhere, until someone thinks to rotate it
  • No way to separate the agent's actions from the person's
  • If it leaks, a person's full access leaked

A credential of its own

What Grenz issues instead

  • Only the actions you granted — anything unlisted is refused
  • Reading and destroying are separate decisions, per action
  • Expires on a deadline, and revokes mid-task without a restart
  • Works only through your proxy, and nowhere else
  • Bounded by budgets, schedules, and approval gates you set

How it works

Three steps, and your agent's code does not change.

Grenz runs on your own infrastructure. The agent needs one thing it does not already have: a different base URL and a token that only works through the proxy.

  1. Credentials go in once

    Stored in an age-encrypted vault, or fetched from HashiCorp Vault. They are injected on the way out to the upstream and never travel back toward the agent — not into a log, an error, an approval payload, or the cloud plane.

  2. You write down what the agent may do

    A YAML policy: allow these actions, require approval for those, deny the rest. Compiled to a deterministic rule object and evaluated purely — anything you did not grant is denied, including when the policy itself fails to load.

  3. Every request is decided in the path

    The engine is embedded in the proxy, so there is no network hop and no third party on the decision path. Allowed calls forward at full speed, risky ones block until a human answers, and everything else is refused before the upstream is contacted.

the request path
  Agent (Claude Code / MCP client / any HTTP tool)
    holds: GRENZ_TOKEN only
        │  HTTP, base URL points at Grenz
  Grenz proxy  http://127.0.0.1:8787
    • authenticate the token         which agent is this?
    • map the request to an action   GET /repos/o/r → repo:read
    • evaluate the policy (embedded) deny > approval > allow
    • approval? block for a human (TTL expires → DENY)
    • check the hourly budget
    • inject the REAL credential ── outbound only ──▶  GitHub
    • log the decision (no secrets, ever)
The real credential appears in exactly one place — the outbound request to the upstream — and never on the response handed back to the agent.

The decision path

One request. Twenty-three gates. In this order, every time.

A permission layer is only as good as the order it checks things in. Grenz's order is fixed and source-visible, and the same code path answers grenz explain. Most of these you will never turn on — they run in sequence anyway, because the order is where the security actually lives.

Authenticate the tokeninvalid_token · agent_token_expired
Decoy token checkdecoy_token
Kill-switchtoken_revoked
Revocation freshnessrevocation_stale
Listener check (socket mode)wrong_listener
Resolve the upstreamunknown_upstream
Decoy upstream checkdecoy_upstream
Map the request to an actionunsupported_request
Delegation attenuationdelegation_scope
Tripwirestripwire
Evaluate the policyexplicit_deny · no_matching_allow
Just-in-time grantswiden only — never override a deny
Schedule windowsschedule_closed
First-use gatingfirst_use_denied
Taint-flow rulesflow_denied
Session target pinningpin_violation
Budget pre-checkbefore a human is woken
Hold for a humanapproval_denied · approval_expired
Budgets (agent · upstream · delegation)budget_exceeded
Content scan of the outbound bodydlp_secret_detected
Egress pinningegress_blocked — before decryption
Fetch the real credentialcredential_missing · vault_error
Forward, credential injectedthe only place it exists
Two worth calling out. Egress pinning runs before the credential is decrypted — a crafted path cannot walk a credential to another host, because the credential is not in memory yet. And the budget is checked before a human is woken, so nobody is asked to approve an action that was going to fail anyway.

This is not a diagram of intent — it is the order in proxy/src/proxy/server.ts. Read it.

Capabilities

The controls a team actually needs before it puts agents in production.

Every item below is shipped and documented, not planned. The source is public if you would rather read it than take our word for it.

Enforcement

Decide per action, not per token

  • Deny by default. Unmatchable requests, malformed policies, and vault failures all resolve to a refusal with a structured reason.
  • Per-action grants across a canonical taxonomy, so reading and deleting are different answers.
  • Target-scoped grants — narrow a permission to specific repositories, projects, or channels.
  • Schedule windows. Time-box an agent to the hours it is supposed to be working.
  • First-use gating. The first time an agent reaches for a new capability, ask.
  • Risk step-up. Escalate a high-risk agent's next allowed action to an approval, instead of cutting it off.

Human control

Keep a person in the loop where it matters

  • Blocking approvals over Slack or the CLI, with a TTL. No answer in the window is a denial.
  • Dual control. Require a quorum of approvers for the actions that warrant two people.
  • Approver context — the request, the agent, and why it was flagged, in the prompt itself.
  • Approval memory. Optionally remember a decision so the same question is not asked twice.
  • Break-glass. A time-boxed emergency path that turns a refusal back into a question for a human — never into a yes.
  • Kill-switch. Revocation takes effect mid-task, before the next upstream call, with no restart.

Credentials & identity

The real secret never reaches the agent

  • age-encrypted vault by default, behind a CredentialStore interface.
  • HashiCorp Vault backend for teams that already run one.
  • Token rotation for agent tokens, without redistributing an upstream credential.
  • Expiring agent tokens. Set a deadline; the token stops working when it passes.
  • Delegation. Agents can spawn sub-agents with a subset of their own grants — revoking the parent cuts off every child.
  • Decoy tokens and upstreams. Honeytokens that cannot be forwarded and alert the moment they are touched.

Containment

Bound the reach, not just the verb

  • Budgets per agent and per hour, with weighting so expensive actions cost more.
  • Per-upstream rate limits so one agent cannot exhaust a shared quota.
  • Content scanning. Outbound bodies are checked for credential shapes — a permitted action can still carry a secret.
  • Egress pinning. A credential can only be sent to its upstream's exact origin — checked before it is decrypted.
  • Response minimization. Cap and trim what comes back, so a read does not become a bulk export.
  • Taint-flow rules and session target pinning — bound an agent to what it has already touched.

Policy operations

Change the rules without holding your breath

  • Lint and simulate. Replay a policy change against recorded traffic before it takes effect.
  • Policy tests — assert allow/deny/approval outcomes as table-driven cases.
  • Draft from plain English and get YAML you can read and edit.
  • Shadow mode and canary. Run a policy in observe-only, or on a slice, before enforcing it.
  • Hot reload, plus history and rollback to any previous version.
  • Blast radius and policy decay — see what a grant really reaches, and what nobody has used.

Fleet & operations

Run it for a team, not just a laptop

  • Signed policy distribution. One policy to a fleet, verified before it is applied.
  • Fleet revocation that propagates and unions with local revocations — a compromised endpoint cannot un-revoke anyone you cut off yourself.
  • SSO via OIDC device flow, with JWKS verification and claim mapping.
  • Named admin tokens with roles, so operators are distinguishable from each other.
  • Prometheus metrics and a live console for what is happening right now.
  • Preflight checks. grenz doctor catches the misconfiguration before it bites.

The policy

One file, readable by whoever has to approve it.

Policies are YAML, compiled to a deterministic rule object and evaluated purely and synchronously. This one is adapted from the github-safe-defaults template in the repository — and it compiles and lints clean, which we check rather than assume.

policy.yamlgithub-safe-defaults
# Reads freely, opens and comments on PRs,
# but cannot merge, delete, or drive CI.
agent: claude-code
on_behalf_of: you@example.com

grants:
  - tool: github
    allow:
      - repo:read
      - pr:read
      - pr:create
      - pr:comment
      - issue:read
    require_approval:
      - pr:merge
    deny:
      - repo:delete
      - repo:write
      - actions:*

budget:
  max_actions_per_hour: 300

# A permitted action can still carry a secret.
dlp:
  scan_bodies: true
  on_match: deny

Written once, verified continuously.

A policy is not a one-way door. Lint it, simulate it against recorded traffic, run it in shadow mode to see what it would have done, then enforce it — and roll back to any previous version if you were wrong.

Operator tooling

Every decision is inspectable — before it happens, not after.

Real output from the commands below. If you want to know why an agent would be stopped, what a leaked token could actually reach, or whether your setup is sound, you ask rather than guess.

grenz explain
$ grenz explain github pr:merge

explain claude-code -> github:pr:merge
  verdict now:  REQUIRE_APPROVAL (approval_required)
  policy:       require_approval — matched
                require_approval "pr:merge"
  kill-switch:  not revoked
  jit grant:    none active for this action
  budget:       agent 0/300 this hour
  approval:     ttl 300s, remember off
The layered decision, in order of precedence — policy, kill-switch, just-in-time grants, budget.
grenz blast-radius
$ grenz blast-radius

blast radius — claude-code [low]

github (github)
  auto-allow: issue:read, pr:comment, pr:create,
              pr:read, repo:read
  requires approval: pr:merge

delegations:
  none active
Everything this agent reaches right now — which is also everything a leaked token would reach.
grenz doctor
$ grenz doctor

 config        grenz.yaml loads and validates
 vault         identity present, vault decrypts
 policy        policy.yaml compiles (1 grants)
 credentials   github: vault key 'github_token'
                missing or empty
 grants        every grant maps to an upstream
 admin token   missing — created on next run
 port          port 8787 free

  1 problem(s), 1 warning(s)
Preflight. It reports the vault key name and whether it is present — never its value.
grenz template list
$ grenz template list

templates:
  github/safe-defaults  [low]     Read freely, open and
     comment on PRs; never merge, delete, or touch CI.
  github/read-only      [low]     Look but never touch.
  linear/safe-defaults  [medium]  Writes need approval.
  slack/safe-defaults   [medium]  Posting needs approval.
  mcp/read-only         [low]     Handshake + reads.
Reviewed starting policies bundled in the binary — grenz add github --template safe-defaults.

Deployment

It runs where your agents run.

A single compiled binary with no runtime dependencies, or a container. Nothing about the decision path requires our infrastructure — or anyone's.

Local

One binary on the machine the agent runs on. Point the agent at 127.0.0.1, or at a unix socket so the listener is reachable only by your own OS user.

Container

Ships as a Docker image for CI runners and orchestrated environments, with the same policy file and the same local decision path.

Fleet

A control plane distributes a signed policy to every proxy, which verifies it before applying it. Revocation propagates. The plane never decides — it only tells proxies what the rules are, so losing it means proxies keep enforcing the last policy they verified.

Upstreams

Native adapters for GitHub over REST, and Linear and Slack over MCP; plus any MCP server speaking JSON-RPC over HTTP. Each maps to a canonical action vocabulary, so one policy language covers all of them.

Secrets

An age-encrypted local file by default, or HashiCorp Vault. Both sit behind one CredentialStore interface, so swapping backends does not touch call sites.

Operations

Prometheus metrics, a live console, and grenz doctor for preflight. Agent identity for operators comes from OIDC SSO, with named admin tokens and roles.

The obvious objection

“Can't I just use a fine-grained token?”

You should. Issue one, select only the repositories it needs, grant the smallest permission set that works, and rotate it. That is genuinely good practice, and Grenz does not replace it — our own docs tell you to do it, then put Grenz in front. It is the floor, not the ceiling. Here is where it stops being enough.

Write access is one switch

An agent that commits needs write access — and the same permission lets it force-push, rewrite history, and delete files. No token can express “read everything, write only to branches you created.”

Grenz: permissions are per action, and can be scoped to specific targets.

A token cannot ask

It is one decision, made once, for the next sixty days. It cannot express “may comment on pull requests, but check with me before merging one.” Tokens have no way to pause and wait for a person.

Grenz: flagged actions block in-flight until a human — or a quorum — answers.

Watching is not stopping

Usage alerts tell you an agent was unusually busy. They do not tell you it deleted a branch, and they cannot intervene. Rotating a leaked token means reissuing and redistributing it everywhere.

Grenz: the decision happens before the action, and revoking takes effect mid-task.

It covers one service

Your agent also touches your issue tracker, your chat, your database, your payment provider. Each has a different permission vocabulary, and some have almost none.

Grenz: one policy, one language, the same controls across every upstream.

A tricked agent is still authorized

An agent that reads a ticket, a page, or a file cannot reliably separate content from instruction. The token does exactly as told, within scope — and nothing in a static credential says “this looks wrong.”

Grenz: reach is narrow enough that a tricked agent has little worth doing, and outbound bodies are scanned regardless.

It is still a person's access

A token is issued by someone and acts as them. There is no budget that applies to the agent but not the human, no schedule, no expiry that does not also lock out its owner.

Grenz: the agent is its own principal, with its own limits and its own lifecycle.

Scope

What Grenz is not.

A security product that overstates its boundary is worth less than one that draws a smaller boundary honestly. So, plainly:

Not a mind reader

Grenz sees the actions an agent takes, not the reasoning behind them. It does not inspect or score a model's intent.

Not an evidence product

The local request log exists so you can see what happened operationally. It is plain SQLite, it is truncatable, and it makes no integrity guarantees — deliberately.

Not a replacement for scoping

It narrows what an agent may do with access it has been given. It cannot narrow what that access is capable of if it escapes. Scope your credentials at the source too.

Not a gateway

Gateways route, meter, and expose agent traffic, and their policy layer is additive by necessity — no configuration means no change in behavior, because a gateway that breaks traffic by default is broken. Grenz inverts that: silence means no. It does not route, does not meter, does not charge, and has nothing to say about turning agent activity into a product. It can be the only thing in your request path, or sit behind a gateway you already run.

Not a sandbox

Grenz controls what an agent reaches through it. Software already running on your machine as you is inside that boundary, and we write the model down rather than let the claim be assumed larger than it is.

Zero to a protected agent in about five minutes.

Install the binary, put a credential in the vault, point your agent at the proxy. The quickstart wraps GitHub; the same three steps wrap any MCP server.

terminal
$ grenz init
$ grenz vault set github_token
$ grenz add github --template safe-defaults
$ grenz run

  Grenz listening on http://127.0.0.1:8787
  agent:        claude-code
  on behalf of: you@example.com
  upstreams:    github (github)
  secrets:      age vault, sealed
  budget:       300/hour
  approvals:    ttl 300s, notify slack
  dlp:          scan_bodies, on_match deny
  shadow:       off
  canary:       off
  policy hist:  1 snapshot(s)