<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Human-in-the-Loop on Damian Galarza | Software Engineering &amp; AI Consulting</title><link>https://www.damiangalarza.com/tags/human-in-the-loop/</link><description>Recent posts from Damian Galarza | Software Engineering &amp; AI Consulting</description><generator>Hugo</generator><language>en-us</language><managingEditor>Damian Galarza</managingEditor><atom:link href="https://www.damiangalarza.com/tags/human-in-the-loop/feed.xml" rel="self" type="application/rss+xml"/><item><title>Governing AI Agents Without Killing Them: What Actually Works in Production</title><link>https://www.damiangalarza.com/posts/2026-04-22-governing-ai-agents-without-killing-them/</link><pubDate>Wed, 22 Apr 2026 00:00:00 -0400</pubDate><author>Damian Galarza</author><guid>https://www.damiangalarza.com/posts/2026-04-22-governing-ai-agents-without-killing-them/</guid><description>Most AI agent governance advice targets boards, not builders. Three failure patterns, real TypeScript examples, and what a CTO should do Monday morning.</description><content:encoded><![CDATA[<p><a href="https://hoolahoop.io/articles/cto-coaching/agentic-ai-governance/">Agentic AI governance for CTOs</a> argues governance needs to come before deployment, not after. The strategic frame is right about what&rsquo;s at stake: organizational accountability, observability, and tool access. But the solutions assume organizational machinery most early-stage teams don&rsquo;t have. A two-person startup running a multi-agent system doesn&rsquo;t need a RACI. It needs a guardrail processor that fails loudly. Leigh names the tension directly: overly restrictive governance drives experimentation underground. Governance that lives in code resolves it — lightweight enough for a seed-stage team, enforceable enough for a regulator.</p>
<p>The piece covers six governance gaps. This post is about three where code-level enforcement most obviously beats policy — tool access, observability, and human-in-the-loop. Cost visibility, shadow AI, and accountability chains are real concerns that deserve their own treatment.</p>
<p>I&rsquo;ve spent the last several months building <a href="/posts/2026-03-06-build-personal-ai-assistant/">a multi-agent AI assistant</a> that runs my consulting business: CRM, email, calendar, invoicing, content pipeline, Slack across two workspaces. Before that, years building software in regulated healthcare, including work on 510(k)-cleared medical device software where every system decision needed an audit trail. &ldquo;We&rsquo;ll add logging later&rdquo; was never an acceptable answer when a regulator could ask to reconstruct any action the system took. That mindset shapes how I think about agent governance. The three patterns below are ones I&rsquo;ve either hit in production or narrowly avoided. Each is a place where code-level governance beats policy-level governance for a team that can&rsquo;t afford a review board.</p>
<h2 id="tool-sprawl-widens-your-blast-radius">Tool Sprawl Widens Your Blast Radius</h2>
<p>MCP server sprawl is named in the original frame as a source of expanded blast radius. The same governance principle lives one layer down, inside the agent&rsquo;s tool definition: every tool an agent can access is a tool it could misuse. An agent with access to email, calendar, invoicing, CRM, and file operations has a blast radius that spans your entire business. A single prompt injection or hallucination can reach tools the agent should never touch. The principle is least privilege at the agent level, not the system level. Each agent should have access to exactly the tools it needs for its role, and nothing else.</p>
<p>What makes this worse is that tool sprawl also degrades the agent&rsquo;s ability to do its job. An agent with 40 tools when it regularly uses 8 faces two compounding problems. Every tool definition consumes context window tokens, space the model can&rsquo;t use for reasoning about the actual task. And the model has to select the right tool from a larger set, which increases the odds of misselection. I&rsquo;ve watched agents pick a vaguely-similar tool over the correct one because the tool list was too long for the model to evaluate carefully. The governance risk and the performance cost come from the same root cause: too many tools in one agent&rsquo;s definition.</p>
<p>In my system, I run a multi-agent architecture where a supervisor delegates to domain-specific agents. I built the first version with a supervisor that had access to everything — why not let it figure out what to use? It worked in demos. In production, both problems showed up immediately: the supervisor&rsquo;s blast radius spanned the entire system, and the model wasted reasoning capacity navigating tools it didn&rsquo;t need.</p>
<p>Here&rsquo;s how I structure it instead. Each agent gets a scoped tool set:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// Each agent declares only the tools it needs
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">const</span> relayAgent <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Agent({
</span></span><span style="display:flex;"><span>  name<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;relay&#34;</span>,
</span></span><span style="display:flex;"><span>  instructions: <span style="color:#f38ba8">relayInstructions</span>,
</span></span><span style="display:flex;"><span>  model: <span style="color:#f38ba8">LOCAL_MODEL_LARGE_THINKING</span>,
</span></span><span style="display:flex;"><span>  tools<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">// Email tools only - no CRM, no calendar, no invoicing
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>    scanInbox,
</span></span><span style="display:flex;"><span>    readEmail,
</span></span><span style="display:flex;"><span>    readEmailThread,
</span></span><span style="display:flex;"><span>    labelEmail,
</span></span><span style="display:flex;"><span>    archiveEmail,
</span></span><span style="display:flex;"><span>    composeEmail,
</span></span><span style="display:flex;"><span>    draftEmail,
</span></span><span style="display:flex;"><span>    replyToEmail,
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">const</span> tempoAgent <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Agent({
</span></span><span style="display:flex;"><span>  name<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;tempo&#34;</span>,
</span></span><span style="display:flex;"><span>  instructions: <span style="color:#f38ba8">tempoInstructions</span>,
</span></span><span style="display:flex;"><span>  model: <span style="color:#f38ba8">FAST_MODEL</span>,
</span></span><span style="display:flex;"><span>  tools<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">// Calendar tools only - no email, no CRM, no invoicing
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>    listCalendarEvents,
</span></span><span style="display:flex;"><span>    getCalendarEvent,
</span></span><span style="display:flex;"><span>    createCalendarEvent,
</span></span><span style="display:flex;"><span>    updateCalendarEvent,
</span></span><span style="display:flex;"><span>    deleteCalendarEvent,
</span></span><span style="display:flex;"><span>    findCalendarFreeBusy,
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>The email agent can&rsquo;t touch the calendar. The calendar agent can&rsquo;t read emails. The invoicing agent can&rsquo;t send Slack messages to the shared workspace. These boundaries aren&rsquo;t documentation. They&rsquo;re structural. An agent literally cannot call a tool it doesn&rsquo;t have.</p>
<p>But tool scoping alone isn&rsquo;t enough. Some tools within an agent&rsquo;s set need additional constraints. My email agent has tools for composing and sending emails. The model can hallucinate plausible-looking recipient addresses, fabricate domains, or construct emails to addresses that don&rsquo;t exist. Instructions alone won&rsquo;t prevent this because the model can reason past them.</p>
<p>Rather than trusting the model&rsquo;s judgment, I enforce this at the framework level using <a href="https://mastra.ai/docs/agents/processors">Mastra&rsquo;s output processors</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// A Mastra processor that blocks emails to fabricated addresses
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">import</span> <span style="color:#cba6f7">type</span> { ProcessOutputStepArgs, Processor } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;@mastra/core/processors&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">const</span> SEND_TOOLS <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Set([<span style="color:#a6e3a1">&#34;compose-email&#34;</span>, <span style="color:#a6e3a1">&#34;reply-to-email&#34;</span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">class</span> EmailSendGuardrailProcessor <span style="color:#cba6f7">implements</span> Processor<span style="color:#89dceb;font-weight:bold">&lt;</span><span style="color:#a6e3a1">&#34;email-send-guardrail&#34;</span><span style="color:#89dceb;font-weight:bold">&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">readonly</span> id <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;email-send-guardrail&#34;</span> <span style="color:#cba6f7">as</span> <span style="color:#cba6f7">const</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  processOutputStep({ toolCalls, abort, messages }<span style="color:#89dceb;font-weight:bold">:</span> ProcessOutputStepArgs) {
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">if</span> (<span style="color:#89dceb;font-weight:bold">!</span>toolCalls<span style="color:#89dceb;font-weight:bold">?</span>.length) <span style="color:#cba6f7">return</span> messages;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">for</span> (<span style="color:#cba6f7">const</span> tc <span style="color:#cba6f7">of</span> toolCalls) {
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">if</span> (<span style="color:#89dceb;font-weight:bold">!</span>SEND_TOOLS.has(tc.toolName)) <span style="color:#cba6f7">continue</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">const</span> to <span style="color:#89dceb;font-weight:bold">=</span> (tc.args <span style="color:#cba6f7">as</span> { to?: <span style="color:#f38ba8">string</span> })<span style="color:#89dceb;font-weight:bold">?</span>.to;
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">if</span> (<span style="color:#89dceb;font-weight:bold">!</span>to) <span style="color:#cba6f7">continue</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#6c7086;font-style:italic">// Block obviously fabricated or placeholder recipients
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>      <span style="color:#cba6f7">if</span> (<span style="color:#94e2d5">/(@example\.com|@test\.com|@placeholder\.)/</span>.test(to) <span style="color:#89dceb;font-weight:bold">||</span> <span style="color:#89dceb;font-weight:bold">!</span>to.includes(<span style="color:#a6e3a1">&#34;@&#34;</span>)) {
</span></span><span style="display:flex;"><span>        abort(
</span></span><span style="display:flex;"><span>          <span style="color:#a6e3a1">`The recipient &#34;</span><span style="color:#a6e3a1">${</span>to<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34; looks like a guessed address. Look up the contact in the CRM first. Never fabricate email addresses.`</span>,
</span></span><span style="display:flex;"><span>          { retry: <span style="color:#f38ba8">true</span> },
</span></span><span style="display:flex;"><span>        );
</span></span><span style="display:flex;"><span>        <span style="color:#cba6f7">return</span> messages;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">return</span> messages;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Processors inspect the step&rsquo;s generated tool calls and can abort execution with a retry hint when something violates a hard rule. If the model hallucinates a recipient address, the guardrail aborts with a message telling the agent to look up the contact in the CRM first. The address never reaches the send tool. No approval card, no prompt-based workaround.</p>
<p>The same principle applies to trust boundaries across workspaces. I run two Slack integrations: one for my private workspace, one for a shared community. The community-facing agent has no browser access, no credential vault, no file system. That&rsquo;s not a policy document. It&rsquo;s a different agent with a different tool set, pointed at a different Slack app.</p>
<p><strong>The pattern:</strong> Don&rsquo;t govern tool access with policies that agents might ignore. Remove the tools from the agent&rsquo;s definition entirely. Governance you can&rsquo;t violate is better than governance you promise to follow.</p>
<h2 id="beyond-tracing-structured-decision-logs-for-agent-governance">Beyond Tracing: Structured Decision Logs for Agent Governance</h2>
<p>You cannot govern what you cannot see. Modern tracing tools like Phoenix Arize, Langfuse, and Mastra Studio show you the full request/response cycle: inputs, outputs, tool calls, latency, and the model&rsquo;s reasoning process. I use Phoenix Arize extensively. It&rsquo;s the first place I look when debugging why an agent picked the wrong tool, hallucinated a parameter, or took an unexpected path.</p>
<p>Tracing is essential, but it answers a specific class of questions: <em>what happened inside the model&rsquo;s reasoning</em>. Governance needs a second layer: structured decision logs that answer <em>what the system decided, what confidence it had, and whether the outcome was correct in your domain context</em>.</p>
<p>This is familiar territory if you&rsquo;ve worked in regulated environments. In healthcare software, particularly anything touching the 510(k) pathway for Software as a Medical Device (SaMD), you don&rsquo;t just log that a record was modified. You log who modified it, when, what the previous value was, and what rule authorized the change. Every action must be reconstructable because a regulator will ask. Agent governance has the same shape, even outside healthcare. The stakeholder asking &ldquo;why did the agent do that?&rdquo; isn&rsquo;t debugging model behavior. They&rsquo;re asking whether the outcome was correct given the business rules, and they need a trail that answers that question without ambiguity.</p>
<p>Here&rsquo;s the distinction in practice. When my email triage agent archives a message, I can see in Phoenix Arize exactly what the model received and how it reasoned about the classification. That&rsquo;s useful for debugging why the model chose &ldquo;archive&rdquo; over &ldquo;escalate.&rdquo; But when I need to answer &ldquo;show me every email that was auto-archived from my inbox last week, what confidence level each had, and which ruleset applied,&rdquo; I need structured logs that are queryable independent of the tracing system.</p>
<p>That means capturing the agent&rsquo;s decision context in a structured, queryable format:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// Each triage decision captures full context for audit
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">interface</span> TriageDecision {
</span></span><span style="display:flex;"><span>  messageId: <span style="color:#f38ba8">string</span>;
</span></span><span style="display:flex;"><span>  subject: <span style="color:#f38ba8">string</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">from</span><span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#f38ba8">string</span>;
</span></span><span style="display:flex;"><span>  classification<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;archive&#34;</span> <span style="color:#89dceb;font-weight:bold">|</span> <span style="color:#a6e3a1">&#34;act&#34;</span> <span style="color:#89dceb;font-weight:bold">|</span> <span style="color:#a6e3a1">&#34;digest&#34;</span> <span style="color:#89dceb;font-weight:bold">|</span> <span style="color:#a6e3a1">&#34;escalate&#34;</span>;
</span></span><span style="display:flex;"><span>  confidence: <span style="color:#f38ba8">number</span>;
</span></span><span style="display:flex;"><span>  mode<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;conservative&#34;</span> <span style="color:#89dceb;font-weight:bold">|</span> <span style="color:#a6e3a1">&#34;full&#34;</span>;   <span style="color:#6c7086;font-style:italic">// Which ruleset applied
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  reason: <span style="color:#f38ba8">string</span>;                  <span style="color:#6c7086;font-style:italic">// Why the agent chose this
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  actionTaken: <span style="color:#f38ba8">string</span>;             <span style="color:#6c7086;font-style:italic">// What actually happened
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  labels: <span style="color:#f38ba8">string</span>[];                <span style="color:#6c7086;font-style:italic">// What labels were applied
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  timestamp: <span style="color:#f38ba8">string</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// Persisted to the database, queryable from the dashboard
</span></span></span></code></pre></div><p>The triage system runs two different modes depending on whose inbox it&rsquo;s scanning. My inbox gets conservative mode (only auto-archives high-confidence machine-generated noise like billing receipts and marketing emails). The AI assistant&rsquo;s inbox gets full mode (four classification categories, auto-archives everything after classification). That modal distinction matters for governance because the blast radius is different. Archiving a marketing email from the assistant&rsquo;s inbox is low stakes. Archiving something from my inbox that I hadn&rsquo;t seen yet is a different conversation.</p>
<p>Beyond decision logging, I track errors with fingerprint deduplication. Every catch block writes to a structured error table with module, message, and context. A background health monitor runs every five minutes, detects stale processes, and escalates to the LLM for analysis when rule-based detection isn&rsquo;t enough. The dashboard surfaces all of this: health banners, error pages with filters, and stale-session badges that go amber after 10 minutes and red after 30.</p>
<p>None of this came from a governance framework. It came from an earlier system I built called Tracewell AI, where agents generated design inputs from source material in a regulated context. Every derivation had to be auditable: &ldquo;show me every design input, which sources the agent pulled from, and its confidence at the time.&rdquo; No trace format could answer that, and I wasn&rsquo;t using one. I built a structured audit log because compliance required it, not because debugging demanded it. That&rsquo;s where I learned the distinction: traces show what the model reasoned; structured logs show what the system decided, under which rules, with what confidence.</p>
<p><strong>The pattern:</strong> Tracing gives you deep visibility into model behavior. Use it. But for governance, pair it with structured decision logs that capture domain-specific context: what was decided, what confidence level, what ruleset applied, and what action the system took. Make both queryable, and make sure someone is actually reviewing them.</p>
<h2 id="human-in-the-loop-the-checkpoint-that-actually-works">Human-in-the-Loop: The Checkpoint That Actually Works</h2>
<p>The most important insight about human-in-the-loop is the &ldquo;rubber-stamp trap.&rdquo; Adding human review to every agent decision is a common starting point. In practice, reviewers get overwhelmed, start rubber-stamping, and the checkpoint becomes theater.</p>
<p>This isn&rsquo;t just a theory. Anthropic recently published <a href="https://www.anthropic.com/engineering/claude-code-auto-mode">research on Claude Code&rsquo;s auto-accept mode</a> that quantifies the problem: users were approving 93% of permission prompts. That&rsquo;s not review. That&rsquo;s muscle memory. Their solution was to replace blanket approval with a tiered system where a model-based classifier evaluates risk and only escalates actions that warrant human attention. The classifier uses a two-stage pipeline (fast filter, then chain-of-thought reasoning) and catches overeager behavior, prompt injection, scope escalation, and honest mistakes while letting routine actions through without friction.</p>
<p>The same principle applies to agent systems. The solution isn&rsquo;t removing human review. It&rsquo;s being precise about <em>where</em> it adds value and <em>what context</em> the reviewer needs to make a real decision.</p>
<p>My system uses a tiered approach. Low-risk actions (reading emails, looking up calendar events, searching the CRM) happen without approval. The agent just does them. High-risk actions go through explicit approval gates using <a href="https://mastra.ai/docs/agents/agent-approval">Mastra&rsquo;s agent approval system</a>. When a tool is tagged with <code>requireApproval: true</code>, Mastra pauses execution at the framework level before the tool runs. The stream emits an approval event with the tool name and arguments, and the tool only executes after an explicit <code>approveToolCall()</code>. This is framework-enforced, not prompt-based, so the model can&rsquo;t reason its way past the gate.</p>
<p>The key design choice is what &ldquo;approval&rdquo; looks like. A generic &ldquo;Agent wants to perform an action. Approve?&rdquo; dialog is useless. The reviewer has no context, so they either rubber-stamp or block everything out of caution. Both outcomes are governance failures.</p>
<p>Here&rsquo;s what a real approval checkpoint looks like for my coding pipeline:</p>
<pre tabindex="0"><code>planning → risk assessment → low risk? ──yes──→ auto-approved → executing
                                ↓ no
                          plan_review → approved → executing
                                ↑              ↓
                            revise ← request changes
</code></pre><p>The agent generates a plan. A risk assessor (two layers: deterministic heuristics for hard stops like <code>DROP TABLE</code> or <code>.env</code> modifications, plus an LLM classifier for everything else) evaluates the plan. Low-risk plans auto-approve and execute immediately. Medium and high-risk plans go to human review with the full plan visible, not just a yes/no prompt.</p>
<p>When I review a plan, I see exactly what the agent intends to do, which files it will touch, and why the risk assessor flagged it. I can approve, request changes (the agent revises and resubmits), or reject entirely. That&rsquo;s a checkpoint with teeth. The reviewer has enough context to make a real judgment call, and the &ldquo;request changes&rdquo; path means the review isn&rsquo;t binary.</p>
<p>For email, the approval is even more specific. When the agent wants to send an email, the approval card shows the full email: recipient, subject, body. I&rsquo;m not approving &ldquo;send an email.&rdquo; I&rsquo;m approving <em>this specific email to this specific person</em>. The context makes the checkpoint real instead of performative.</p>
<p>The less obvious lesson: the approval system itself can break in ways that look like it&rsquo;s working. I discovered that tagging certain tools with <code>requireApproval</code> caused my supervisor agent to avoid delegating to the sub-agent entirely. The supervisor model saw that the delegation path was &ldquo;approval-gated&rdquo; and hallucinated reasons not to use it. The approval mechanism was technically present but functionally disabled because the model routed around it. I only caught this by checking the traces (see: observability matters).</p>
<p><strong>The pattern:</strong> Approval checkpoints work when three conditions are met. The reviewer sees the full context of the action, not just a generic prompt. Low-risk actions bypass review entirely so the reviewer isn&rsquo;t fatigued. And the system is monitored to ensure the approval path is actually being exercised, not silently avoided.</p>
<h2 id="governance-as-code-defense-in-depth">Governance as Code: Defense in Depth</h2>
<p>Each of the previous patterns (tool scoping, guardrail processors, decision logs, approval gates) is a single layer. None of them is sufficient alone. The real value shows up when you stack them.</p>
<p>Take sending email as the running example. You&rsquo;ve already seen the individual layers: the email guardrail processor that blocks fabricated recipients, and the approval gate that pauses execution for human review. Here&rsquo;s how they combine with two additional layers into a defense-in-depth stack:</p>
<ol>
<li><strong>Tool API design</strong> forces an explicit <code>sender</code> parameter (&ldquo;emma&rdquo; | &ldquo;damian&rdquo;) with no default. The caller must deliberately choose which account sends.</li>
<li><strong>Guardrail processor</strong> blocks fabricated or placeholder recipients before the tool executes. Hard abort, no workaround.</li>
<li><strong>Framework-level approval gate</strong> (<code>requireApproval: true</code>) pauses execution and surfaces the full email for review: recipient, subject, body, sender.</li>
<li><strong>Client-level enforcement</strong> in <code>sendEmail()</code> requires an explicit <code>userEmail</code> argument. No fallback, no default. If the parameter is missing, it throws.</li>
</ol>
<p>Each layer is independent. If the model hallucinates a recipient, Layer 2 catches it. If it tries to send without approval, Layer 3 blocks it. If somehow the tool args are malformed, Layer 4 throws. A bypass at one layer doesn&rsquo;t compromise the others.</p>
<p>That&rsquo;s what governance as code means. The constraints are enforced by the system, verified by tests, and visible in the codebase, not buried in a Confluence page. This is one of the <a href="/posts/2026-03-25-four-patterns-that-separate-agent-ready-codebases/">dimensions that separate agent-ready codebases</a> from ones that break under real workloads. Frameworks like <a href="https://mastra.ai/docs/agents/guardrails">Mastra</a> give you the primitives: guardrail processors for hard rules, approval gates for human review. Your job is to wire them into a layered defense that matches your risk profile.</p>
<h2 id="what-id-tell-a-cto-to-do-monday-morning">What I&rsquo;d Tell a CTO to Do Monday Morning</h2>
<p>If you&rsquo;re leading a team that&rsquo;s deploying agents, here&rsquo;s where to start:</p>
<p><strong>Audit your tool surfaces.</strong> For every agent in production, list the tools it has access to and ask: does this agent need all of these? Every unnecessary tool is expanded blast radius and wasted context window. Scope them down. You&rsquo;ll likely see better tool selection as a side effect.</p>
<p><strong>Add structured decision logging to your highest-stakes agent action.</strong> You probably already have LLM tracing. Pick the one action where you&rsquo;d need to explain &ldquo;why did the agent do that?&rdquo; to a stakeholder, and add structured logs that capture the decision context: inputs, classification, confidence, action taken. Make it queryable from your dashboard, not buried in trace spans.</p>
<p><strong>Pick your highest-risk action and build a real checkpoint.</strong> Not a generic approval dialog. An approval flow that shows the reviewer the full context of the action. Frameworks like <a href="https://mastra.ai/docs/agents/agent-approval">Mastra</a> provide the primitives. One real checkpoint is worth more than twenty rubber-stamp prompts.</p>
<p><strong>Move one governance rule from documentation to code.</strong> Find a constraint that&rsquo;s currently a line in a README or a team agreement. Encode it as a guardrail processor, a test, a structural boundary. Something that fails loudly when violated rather than depending on an agent reading and following instructions.</p>
<p>These are afternoon-sized tasks, not quarterly initiatives. That&rsquo;s the point. The original frame is right that governance has to come before agents ship at scale. But &ldquo;before&rdquo; doesn&rsquo;t mean organizational review boards. It means constraints in code that ship with the agent. Each of these gives you something concrete: a tighter tool surface, a queryable decision log, an approval checkpoint that someone actually uses, or a constraint that enforces itself without depending on an agent&rsquo;s good behavior.</p>
<p>If you want a faster read on where you stand, I built a companion <a href="/agent-governance-scorecard/">Agent Governance Scorecard</a> — 30 yes/no questions across the four dimensions above. It takes about ten minutes and tells you which layer to fix first.</p>
<hr>
<p>These aren&rsquo;t theoretical patterns. They&rsquo;re the same techniques I apply when working with early-stage teams to formalize their agent architecture.</p>
<p><strong>Most early-stage teams I talk to have agents in production and governance that&rsquo;s still catching up.</strong> The gap between &ldquo;it works&rdquo; and &ldquo;I can explain why it did that&rdquo; is where real risk lives — and it&rsquo;s where investors, partners, and your first enterprise customer will start asking questions. If that sounds familiar, <a href="/pages/meet/">book a free 30-minute strategy call</a>. I&rsquo;ll walk through your agent architecture, identify the highest-risk tool surfaces, and give you a prioritized action plan: what to lock down first, what can wait, and which of these patterns fits your system. No slide decks. Just a concrete roadmap you can start executing the same week.</p>
<h2 id="further-reading">Further Reading</h2>
<ul>
<li><a href="https://hoolahoop.io/articles/cto-coaching/agentic-ai-governance/">Agentic AI Governance: What CTOs Need to Know</a> — a solid overview of the organizational and strategic side of agent governance</li>
<li><a href="https://mastra.ai/docs/agents/agent-approval">Mastra Agent Approval</a> and <a href="https://mastra.ai/docs/agents/guardrails">Guardrails</a> — the framework primitives used in the examples above</li>
<li><a href="https://www.anthropic.com/engineering/claude-code-auto-mode">Claude Code Auto Mode: A Safer Way to Skip Permissions</a> on how Anthropic built tiered approval into Claude Code</li>
<li><a href="/posts/2025-11-06-build-efficient-mcp-servers-three-design-principles/">Build Efficient MCP Servers: Three Design Principles</a> on scoping what agents can access at the tool design level</li>
<li><a href="/posts/2026-02-17-how-ai-agents-remember-things/">How AI Agents Remember Things</a> on the memory and context systems that feed agent decisions</li>
<li><a href="/posts/2026-03-25-four-patterns-that-separate-agent-ready-codebases/">Four Dimensions of Agent-Ready Codebase Design</a> on building codebases that support reliable agent output</li>
<li><a href="/posts/2026-02-05-mcps-vs-agent-skills/">MCPs vs Agent Skills</a> on architecture decisions that shape agent capabilities</li>
</ul>
]]></content:encoded></item></channel></rss>