How Brex built its AI agent policy by watching agents

Your team just wired Claude into production. It has an API key, an OAuth token to your CRM, and a service account for your database. Two weeks in, you find it created 47 duplicate contacts, revoked a legitimate user's session, and burned $340 in tokens retrying a malformed webhook. The guardrails you wrote on day one didn't catch any of it — because you were guessing what agents would do wrong.
Brex hit this exact wall at enterprise scale, and their response is worth stealing. Instead of writing an agent security policy up front, they built an internal platform that watches what agents actually do with real credentials, then codifies rules from observed behavior. This post breaks down why that inversion works, how to apply it if you're a two-person team or a fifty-person company, and what to put in place before your first agent touches production.
Why writing agent rules first doesn't work
Traditional security policy assumes you know the threat model before the system runs. That assumption breaks for agents. An agent is a probabilistic actor holding real credentials, chaining tool calls in an order you didn't plan, across a context window you don't fully control. Rules written from first principles miss the failure modes that only appear under real workloads.
Here's what typically happens when teams write rules first:
- The allowlist is either too narrow (agent fails on legitimate tasks, users disable guardrails to unblock work) or too broad (agent exfiltrates data through a tool combination nobody predicted).
- Rate limits are set by intuition, not observed p95 behavior, so they either throttle real work or let a runaway loop rack up cloud bills before anyone notices.
- Prompt injection tests cover known payloads, but the actual attack surface is any external content the agent reads — customer support tickets, scraped web pages, PDFs uploaded by users.
The Brex approach flips this. You instrument first. You give the agent constrained but real access. You watch every tool call, every credential use, every retry. Then you write the policy against the ground truth of what the agent tried to do.
The observation-first playbook, in five steps
The pattern maps cleanly onto any stack, whether you're running Claude Code for a solo SaaS or an internal LangGraph deployment. Here's the sequence:
- Issue scoped, short-lived credentials. Not your personal API key. A service account or OAuth app with a minimal starting scope and a TTL measured in hours, not months.
- Log every tool call to structured storage. Tool name, arguments, response size, latency, credential used, session ID, upstream user. Ship it to a queryable store (BigQuery, ClickHouse, even Postgres with a JSONB column).
- Run the agent against real workloads in a shadow or low-blast-radius mode. Read-only mirrors, staging tenants, a small percentage of live traffic with a human in the loop.
- Review the log daily for the first two weeks. Look for tool-call sequences you didn't design, credential reuse patterns, high-volume retries, and any request that touches PII.
- Codify guardrails from what you saw. Now you have evidence: this agent calls
create_contact3x for everysearch_contact, it retries failed webhooks 12 times, it occasionally tries to escalate its own permissions. Write policy against that reality.
The point is that step 5 is last, not first. You cannot write a good policy without steps 1-4.
Minimal instrumentation: log every tool call
You don't need a vendor platform to start. Here's a Python decorator that captures the fields you actually need for post-hoc analysis:
import json
import time
import uuid
from functools import wraps
from datetime import datetime, timezone
def observe_tool(logger):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
call_id = str(uuid.uuid4())
started = time.monotonic()
record = {
"call_id": call_id,
"tool": fn.__name__,
"args": _redact(kwargs),
"session_id": kwargs.get("_session_id"),
"credential_ref": kwargs.get("_credential_ref"),
"ts": datetime.now(timezone.utc).isoformat(),
}
try:
result = fn(*args, **kwargs)
record["status"] = "ok"
record["response_bytes"] = len(json.dumps(result))
return result
except Exception as e:
record["status"] = "error"
record["error_class"] = type(e).__name__
raise
finally:
record["latency_ms"] = int((time.monotonic() - started) * 1000)
logger.emit(record)
return wrapper
return decorator
Two rules for the _redact function: strip anything that looks like a secret (keys, tokens, passwords), and hash any PII field before it hits the log. You want to be able to query "how often did the agent touch customer email X" without keeping raw emails in an analytics store.
Once this is running, a single SQL query answers most of the interesting questions:
select
tool,
count(*) as calls,
count(distinct session_id) as sessions,
approx_quantiles(latency_ms, 100)[offset(95)] as p95_ms,
countif(status = 'error') / count(*) as error_rate
from agent_tool_calls
where ts > current_timestamp() - interval 24 hour
group by tool
order by calls desc
This is the report you should be reading every morning for the first two weeks after any new agent goes live.
Credentials: what "real" access should look like
The failure mode Brex saw — and every team eventually sees — is agents given credentials with the same lifetime and scope as human employees. That doesn't work. An agent that calls a tool 400 times an hour needs a credential model built for that pattern.
Practical rules that hold up in production:
| Concern | Human developer | Production agent |
|---|---|---|
| Credential type | Personal token / SSO | Service account or OAuth app, per agent |
| Scope | Broad (dev role) | Minimal, per-tool, expanded from logs |
| Lifetime | 90 days | 1-24 hours, auto-rotated |
| Storage | 1Password / env | Secrets manager, short-lived issuance |
| Revocation | Manual | Automated on anomaly |
| Audit granularity | Login events | Every tool call, every arg |
The mistake I see most often on solo/SMB teams is reusing the founder's personal API key as the agent's credential. It works on day one. On day thirty, the agent has done something you can't easily attribute or revoke without breaking your own access.
If you're on AWS, the shape looks like this:
# One IAM role per agent identity, assumed via STS with 1-hour TTL
AgentInvoiceProcessor:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal: { Service: agents.internal }
Action: sts:AssumeRole
Condition:
StringEquals:
sts:ExternalId: !Ref AgentSessionId
MaxSessionDuration: 3600
Policies:
- PolicyName: InvoiceReadWrite
PolicyDocument:
Statement:
- Effect: Allow
Action: [ s3:GetObject, s3:PutObject ]
Resource: arn:aws:s3:::invoices-processed/*
The external ID and one-hour cap are the important parts. You want every agent session to be a distinct, traceable credential you can revoke without touching anything else.
The guardrails you write after two weeks of watching
After you've logged real traffic, the policy writes itself. In practice, most teams find they need these categories, in this order of importance:
1. Tool-sequence rules. Certain tool combinations are always suspicious. "Read customer PII → external HTTP request" is the classic. "List all users → update user" without an explicit user ID in the session context is another. These are detectable in your logs as sequence patterns, not single events.
2. Volume anomaly detection. From the p95 numbers you now have, set hard caps at roughly 3-5x observed p95 with alerting at 2x. This catches the retry loop that would otherwise burn $2,000 in tokens overnight. If p95 for send_email is 8 calls per session, alert at 16 and block at 40.
3. Argument content filters. You saw what the agent actually sends. Now you can write regex-based blockers for the specific injection patterns that showed up — usually "ignore previous instructions" variants embedded in ticket bodies, plus base64 blobs the agent shouldn't be forwarding.
4. Human-in-the-loop triggers. Certain actions need approval even if they're "allowed." Refunds over a threshold. Any write to a customer record you flagged as high-value. Sending outbound email to more than N recipients in a session.
Codified as a middleware check, it looks like this:
def enforce_policy(call, session):
if call.tool in HIGH_RISK_TOOLS and session.tool_count(call.tool) > CAPS[call.tool]:
raise PolicyViolation(f"{call.tool} exceeded session cap")
if _sequence_matches(session.recent(5), SUSPICIOUS_SEQUENCES):
session.pause_and_notify("suspicious tool sequence")
raise PolicyViolation("sequence blocked")
if call.tool in HUMAN_APPROVAL_TOOLS:
return await_approval(call, session)
if _contains_injection_marker(call.args):
log_and_strip(call)
return call
None of this is exotic. It's the boring part that separates agents that ship from agents that get quietly turned off after an incident.
What to watch for that isn't in the logs
Observation-first has a blind spot: the model's reasoning. Logs show you tool calls, not why the agent made them. A few habits that fill the gap:
- Capture the model's chain-of-thought or plan output alongside each tool call, at least during the observation phase. Store it separately from the structured logs — it's high-volume and often contains raw PII the agent read.
- Sample sessions for human review. Not just the failed ones. Random 1-2% of successful sessions, read end-to-end. This is where you'll find the agent doing something correct but for a reason that won't generalize.
- Track prompt-injection candidates from external inputs. Any content the agent ingests from outside your trust boundary (support tickets, web pages, uploaded files) is an attack vector. Log a hash and length; when an incident happens, you can correlate.
- Watch for silent degradation. Model providers change models under the same name. A jump in error rate or tool-call volume with no code change on your side often traces back to a provider-side update. The daily report catches this.
Anthropic's own guidance on building with Claude reinforces the same point: tool use is where model behavior meets real systems, and it's where observation matters most.
How BizFlowAI approaches this
We run this exact pattern for clients rolling out Claude agents — solo founders automating invoice processing, small ops teams handling lead qualification, SaaS builders wiring agent-driven onboarding. Every deployment starts in observation mode: scoped service accounts, structured tool-call logging, a two-week review window before any policy gets locked in. We've watched more than one agent try things we'd never have thought to block on day one, and every one of those became a codified rule in the next iteration.
The economics work for small teams because you don't need a dedicated security engineering function. You need a decorator, a queryable log store, and thirty minutes a day for two weeks. If you want a second set of eyes on the setup before you point an agent at production, book a discovery call and we'll walk through the specific credentials, tools, and observation stack for your workflow.
A 30-day rollout you can actually run
If you're starting from zero, here's the schedule that fits a solo or small-team operator without pulling anyone off customer work:
Week 1 — Instrumentation. Wrap every tool the agent will use with the logging decorator. Set up the log destination. Issue one scoped service account per agent identity. Do not connect the agent to production yet.
Week 2 — Shadow mode. Point the agent at read-only mirrors or a staging tenant. Run realistic workloads through it — replay a day of support tickets, a batch of real invoices with customer names stripped. Read the logs daily. Fix the obvious errors before they become policy problems.
Week 3 — Constrained production. Enable writes on a small percentage of real traffic (10-20% is a reasonable start), with human approval on high-risk tool calls. Keep reading logs daily. Write the first pass of guardrails from what you've seen.
Week 4 — Codified policy, expanded traffic. Guardrails are enforced in middleware. Approval loop stays for the high-risk actions. Ramp to full traffic. The daily log review becomes a weekly one, backed by automated anomaly alerts.
At the end of thirty days you have an agent in production with a policy grounded in evidence, credentials scoped tightly enough that an incident can be contained, and logs detailed enough that you can answer "what did the agent do at 3 AM on Tuesday" without guessing.
That's the whole trick. Write rules from observed behavior, not imagined threats. It's slower on day one and dramatically faster by day sixty, because you're not spending your time either debugging over-tight guardrails or cleaning up incidents your over-loose ones missed.
Work with BizFlowAI
If you'd rather have this built for you, that's what we do: production AI automation for solo founders and small teams — agents, integrations, and document pipelines that actually ship.
Book a free discovery call — 30 minutes, we map the highest-ROI automation in your workflow. No pitch deck, just engineering.
More guides like this on the BizFlowAI blog.
Frequently asked questions
Why should you observe AI agents before writing a security policy?
Agents are probabilistic actors that chain tool calls in orders you didn't plan, so first-principles rules miss real failure modes. Brex found that watching agents with real credentials in low-blast-radius environments reveals the actual sequences, retry loops, and permission escalations that occur. Policies written from this observed behavior catch issues that upfront allowlists miss. This inverts the traditional security workflow of writing rules first.
What credentials should you give an AI agent in production?
Never reuse a personal API key or long-lived human token. Issue a dedicated service account or OAuth app per agent, scoped to a minimal set of tools, with a lifetime of 1-24 hours and automated rotation. On AWS, use an IAM role assumed via STS with a one-hour MaxSessionDuration and a unique ExternalId per session so you can revoke individual agent sessions without breaking anything else.
What fields should you log for every AI agent tool call?
Capture the tool name, redacted arguments, session ID, credential reference, timestamp, response size in bytes, latency in milliseconds, status, and error class on failure. Ship these to a queryable store like BigQuery, ClickHouse, or Postgres with a JSONB column. Redact secrets and hash PII fields before storage so you can query patterns without keeping raw sensitive data. This lets you compute p95 latency, error rates, and tool usage per session.
How do you set rate limits for an AI agent?
Base limits on observed p95 behavior from at least two weeks of real traffic, not intuition. A practical rule is to alert at 2x the observed p95 and hard-block at roughly 3-5x. For example, if p95 for send_email is 8 calls per session, alert at 16 and block at 40. This catches runaway retry loops before they burn thousands in tokens overnight.
What tool-call patterns indicate a compromised or misbehaving AI agent?
The classic red flag is reading customer PII followed by an external HTTP request, which suggests data exfiltration. Listing all users followed by an update without an explicit user ID in session context is another. High retry counts on failed calls, credential reuse across sessions, and any attempt to escalate the agent's own permissions also warrant investigation. Detect these as sequence patterns in logs, not as single-event rules.