Securing the Agent Harness

Start with a number. An agent that opens 1,000 pull requests a week at a 1% vulnerability rate ships 10 new vulnerabilities every week — quietly, confidently, indefinitely. That’s the uncomfortable arithmetic of autonomy: a small per-action error rate, multiplied by machine-speed volume, becomes a steady stream of security holes. A human writing ten PRs a day can’t produce vulnerabilities fast enough to matter at that scale. An agent fleet can.

This reframes the whole problem. Security for AI agents isn’t a model-quality question you can solve with a better prompt. It’s a systems question, and the answer to “how do I keep this safe?” is the same answer we’ve used for every other dangerous-at-scale system: build the controls into the workflow so they can’t be skipped, and contain the blast radius for when — not if — something goes wrong. As the field’s bluntest formulation puts it: autonomy without security is an automated vulnerability.

You’re securing a harness, not a model

The single most useful mental shift in this entire topic: you cannot fully secure the model, so secure the harness instead.

The “harness” is everything around the model — the runtime scaffold that turns a text predictor into something that acts: its tools, its permissions, its credentials, its sandbox, its network access, the context you feed it, and the gates between it and the real world. The model is the part you can’t make trustworthy (more on why in a moment). The harness is the part you can engineer, and it’s where all real agent security lives.

Diagram illustrating the concept of securing the harness rather than the model, highlighting tools, sandbox, credentials, context & data, network, and human & policy gates.

Why can’t you just secure the model? Because of a result every agent builder should internalise. A 2026 joint study by OpenAI, Anthropic, and Google DeepMind — titled, ominously, “The Attacker Moves Second” — found that under adaptive attack conditions, every published prompt-injection defence was bypassed with success rates above 90%. You cannot prompt-harden your way to safety. The only robust posture is to assume the model will be compromised and build controls outside it that contain what a compromised model can do. This is the same shift the security industry made with malware a decade ago: stop trying to prevent every execution, assume breach, and focus on segmentation, least privilege, and blast-radius containment.

The blast radius principle

Here’s the principle that makes the harness concrete: an agent’s potential damage equals that of an employee holding the same credentials. If the agent can merge to main, it can ship a vulnerability. If it holds a production database token, it can drop a table. If it can make outbound network calls, it can exfiltrate data. The blast radius scales precisely with the privileges you hand it — and in 2026 that’s not hypothetical. In April 2026, a coding agent reportedly wiped a startup’s production database. The damage wasn’t exotic; it was simply an over-privileged agent doing a destructive thing with credentials it should never have held.

So the first question for any agent isn’t “is it smart enough?” — it’s “what’s the worst thing it can do with what we’ve given it?”

Prompt injection: the attack that makes it real

The mechanism that turns “the model might be wrong” into “the model is now working for an attacker” is prompt injection — OWASP’s #1 LLM vulnerability, present in an estimated 73% of production deployments. It comes in two flavours:

  • Direct injection: malicious instructions in the user’s own message.
  • Indirect injection: malicious instructions hidden in external content the agent processes — web pages, documents, code comments, dependency docstrings, issue and PR bodies, API responses. This is the dangerous one for coding agents, because a coding agent’s entire job is to ingest untrusted content: the repo, retrieved docs, web search results, package documentation. Every one of those is an attacker-controllable input channel.

In agentic systems this gets worse, not better. The OWASP Top 10 for Agentic Applications 2026 introduces ASI01: Agent Goal Hijack — a single injected instruction doesn’t just corrupt one output, it reprograms the agent’s plan, redirecting its multi-step behaviour, triggering privileged tool calls, and persisting across the workflow. A real example landed in May 2026: a CVSS-10 vulnerability in Gemini CLI where a malicious package injected prompts through code comments and docstrings, causing the agent to execute arbitrary commands it believed were legitimate — indirect injection delivered straight through the software supply chain.

The lethal trifecta

The cleanest model for when prompt injection becomes a breach is Simon Willison’s lethal trifecta. An agent is structurally exploitable when it has all three of:

  1. Access to private data (your repo, your secrets, your database).
  2. Exposure to untrusted content (anything an attacker can influence — issue bodies, web pages, dependencies).
  3. The ability to communicate externally (an outbound channel — an API call, a curl, a public PR comment).
Venn diagram illustrating the concept of 'The lethal trifecta' with three circles labelled 'Private data', 'Untrusted content', and 'External comms', highlighting 'EXFILTRATION' in the overlapping centre.

When all three overlap, a single poisoned input becomes an exfiltration pipeline: the agent reads the malicious instruction, pulls your secrets, and ships them out the door — a confused deputy moving faster than your monitoring can react. Remove any one leg and the attack collapses.

And here’s the catch that makes this urgent for developers: a CI/CD pipeline contains the entire trifecta by default. Workflow secrets sit in the runner’s environment, every public issue and PR body is attacker-controlled input, and tools like gh or curl are ready-made exfiltration channels. The Gemini CLI exploit chained exactly this — injection through a public issue, credentials lifted from .git/config on disk, then a pivot to a token with write access. If you’ve added an AI triager or reviewer to your CI, you may have shipped a trifecta without noticing.

Why bolt-on security fails

Put the pieces together and the conclusion is forced. At 10 vulnerabilities a week, a quarterly pentest is laughably out of phase — you’d accumulate ~130 vulnerabilities between assessments. When the model can be compromised >90% of the time by an adaptive attacker, an after-the-fact review of its output misses the architectural problem entirely. And prompt injection, the field’s consensus holds, needs architectural fixes, not bolt-on filters. The numbers say teams know this and aren’t acting on it: an independent 2026 assessment of 100 production agents found only 11% passed a baseline security assessment, and 57% of organisations lacked the visibility to even audit their agents for the trifecta.

Security that’s added after the agent works is security that’s always behind the agent. The only posture that keeps pace is one where the controls are part of the harness from the first commit.

Building the Secure Harness

We’ve previously reached a conclusion, a design principle: assume the model is compromised, and contain the blast radius with controls outside it. Now, we’ll turn that principle into concrete architecture. The unifying idea is defence in depth — stack independent layers so an attack has to defeat every one of them in series, not just slip past a single clever filter. No layer is sufficient alone (adaptive attacks bypass even good classifiers >85% of the time); stacking them is how production contains risk.

The organising constraint: the Rule of Two

Before the individual controls, the design rule that ties them together. Meta’s “Agents Rule of Two” (October 2025) is the most actionable security constraint in agent design: within a single session, an agent should have at most two of these three properties:

  • (A) processes untrusted input,
  • (B) has access to sensitive systems or private data,
  • (C) can change state or communicate externally.
Diagram illustrating 'The Rule of Two' concept for managing input and access levels, featuring sections A (Untrusted input), B (Sensitive access), and C (External/write), with safety combinations and a warning about the lethal trifecta.

This is the lethal trifecta restated as a build rule. Keep all three apart and the trifecta never assembles. In practice that often means splitting one risky agent into two safe ones — a read-only agent that ingests untrusted content but holds no secrets and can’t reach the network, handing structured results to a write-capable agent that never touches untrusted input. Separating read and write capability is one of the highest-leverage architectural moves you can make.

With that frame, here are the layers.

Layer 1 — Least privilege and least autonomy

The blast-radius principle from before has a direct corollary: give the agent the minimum tools, data, and credentials its task requires, and no more. Treat an agent exactly like a service account — narrowly scoped, audited, and reviewed. Start each new agent with the narrowest possible capability set and a small, well-defined task where the blast radius of failure is contained, then expand deliberately. “What’s the maximum access available” is the wrong default; “what’s the minimum this task needs” is the right one.

Layer 2 — Sandboxing and isolation

Assume the agent will, at some point, run hostile code — generated by a poisoned dependency, an injected instruction, or its own mistake. So run everything it does in an isolated, ephemeral environment with hard resource limits and the ability to roll back.

The 2026 consensus on strength: default to microVMs for untrusted code (Firecracker — the technology behind AWS Lambda, exposed by platforms like E2B — gives each sandbox its own kernel and network namespace, so a guest-kernel vulnerability can’t reach the host), and relax to gVisor or plain containers only when your threat model justifies it. Make sandboxes per-session and disposable with clean teardown so nothing persists between runs, and set hard memory, disk, and CPU limits to stop resource-exhaustion attacks. For coding agents this dovetails with the isolation you already want for parallelism: each agent gets its own throwaway workspace.

Layer 3 — Network egress control

This is the layer that directly kills the trifecta’s exfiltration leg. Agents should run on a zero-trust network: default-deny all outbound traffic, and allowlist only the specific endpoints the task requires. Add DNS restrictions to prevent command-and-control lookups, segment the agent’s network from production systems and sensitive data stores, and watch for anomalies (connections to low-reputation domains, unusually large outbound POSTs). If the agent can only reach three approved hosts, a stolen secret has nowhere to go.

Layer 4 — Scoped credentials and the broker pattern

Credentials are the crown jewels, and there are two distinct goals: keep them out of the model’s context (so the LLM provider never sees them) and out of the agent runtime entirely (so a compromised agent can’t read them). The pattern that achieves both is a credential broker: the agent never holds a real secret — it calls a tool, and a separate broker process, outside the agent’s reach, attaches the short-lived, narrowly-scoped credential and makes the actual API call.

# The agent asks the broker to act; it never sees the credential.
class CredentialBroker:
def call(self, tool: str, args: dict, session: Session) -> Result:
policy.authorize(tool, args, session) # per-tool PERMIT/DENY
cred = self.vault.mint(tool, ttl_seconds=300, # short-lived, scoped
scope=min_scope_for(tool))
try:
return execute(tool, args, cred) # broker makes the call
finally:
self.vault.revoke(cred) # gone after one use

Credentials are minted just-in-time, scoped to the single operation, and expire in minutes. Even a fully hijacked agent can’t exfiltrate a secret it never possessed — and a stolen short-lived token is worthless minutes later.

Layer 5 — The tool gateway

The agent’s reasoning must never be the thing that decides whether a dangerous action is allowed — because we’ve previously established that its reasoning can be hijacked. So put all tool access behind a single policy gate that enforces authorisation, consent, filtering, and audit centrally, so nothing bypasses policy. Per-tool PERMIT/DENY rules (the Cedar policy engine and the cedar-for-agents pattern are built for this) are evaluated against a declared rule — principal, action, resource, conditions — independent of the agent’s reasoning. Expose tools with fixed schemas so the agent can’t improvise novel calls, and wrap your most sensitive integrations as sealed tools running in a separate container. An MCP gateway is a natural home for this — it’s the chokepoint where you sanitise context and enforce allowlists between the agent and everything it can touch.

def gateway(call: ToolCall, session: Session) -> Result:
if not schema_valid(call): # fixed schema — no improvising
return deny("schema violation")
decision = cedar.evaluate(call, session) # policy, not model judgment
if decision != "PERMIT":
return deny(decision)
if call.is_high_impact: # irreversible / sensitive
require_human_approval(call) # tiered gate
audit_log.record(call, session) # decision, not just output
return broker.call(call.tool, call.args, session)

Notice the gateway is also where the later layers attach: human approval for high-impact actions, and an audit record of every decision.

Putting the harness together

Stacked, these layers form a harness where a compromise stays contained:

A flowchart illustrating a secure harness system for defence in depth, featuring components such as a microVM sandbox, human approval, a tool gateway, a credential broker, and an egress allowlist.

Trace an attack through it. A poisoned dependency injects an instruction. The hijacked agent tries to exfiltrate secrets — but it holds none, because the broker does (Layer 4). It tries to call a destructive tool — but the gateway’s policy denies it regardless of the agent’s “reasoning” (Layer 5). It tries to phone home — but egress is default-deny (Layer 3). It tries to corrupt the host — but it’s in a disposable microVM (Layer 2). Every layer the attack defeats, another stands behind it. That’s defence in depth, and it’s the difference between a bad session and a production incident.

Security in the Workflow

A secure harness is necessary but not sufficient. 10 vulnerabilities a week from a 1%-error agent fleet is a flow problem, and flow problems need controls in the flow. A harness you configure once protects the runtime; it doesn’t catch the vulnerability in PR #847 on Tuesday afternoon. For that, security has to live in the workflow itself, running on every action the agent takes, automatically. This is the “built-in, not bolted-on” thesis made operational.

Gate every agent PR like untrusted code

The agent’s output is untrusted code — we’ve already established the model can be hijacked, so its commits deserve the same suspicion as a pull request from an anonymous outside contributor:

Diagram illustrating security gates in the agent workflow, including agent PR, deterministic security gates with various checks, risk routing based on security impact, and audit logging for decisions.

The deterministic gates run on every agent PR and block on failure:

  • SAST (static application security testing) for vulnerability patterns — the direct defence against the 10-vulns-a-week problem.
  • Secret scanning so a credential never lands in a commit.
  • Dependency and CVE scanning, plus a hallucinated-package check — does every new import actually exist and is it the real package, not a slopsquatted look-alike registered to catch exactly the names models invent (the supply-chain attack).
  • IaC scanning for misconfigured infrastructure.

Because these are deterministic, they make trustworthy hard gates — no human time spent on a leaked key or a known-vulnerable dependency. This is shift-left security applied to a contributor that never sleeps.

Tier human oversight so it survives contact with volume

Human approval is the control that stops a hijacked agent’s high-impact action — but naive human review doesn’t scale to agent volume, and worse, it breeds approval fatigue: ask a human to rubber-stamp 200 trivial changes and they’ll rubber-stamp the one dangerous one too. The fix is risk-based tiering, the same Green/Yellow/Red routing, applied to security impact:

  • Low-risk (docs, tests, isolated non-sensitive code) → auto-merge with notification.
  • High-impact / irreversible (auth, payments, data migrations, production deploys, anything touching the paths from your risk map) → mandatory human approval, every time.

Tiering concentrates scarce human attention exactly where the blast radius is largest, and keeps it sharp by not wasting it everywhere else. Even a lightweight approval step — a Slack confirm, a required reviewer on protected paths — is a meaningful gate. The goal is that no irreversible action happens without a human, without drowning humans in reversible ones.

Audit decisions, not just outputs

You detect a hijacked agent not by reading its final answer but by watching its behavior. So log the agent’s decisions — every tool call, every delegation, every retrieval, every hand-off — not just its outputs. Baseline what a normal tool-call sequence looks like, and alert on deviations: a triage agent that suddenly reads .git/config and opens a network connection is mid-exploit, and only a decision-level trace shows it.

This audit trail does double duty. It’s your incident-response timeline, and it’s your compliance evidence — the record of which agent accessed which data, under which policy, authorized by which human, at what time. (Treat memory writes as security events too: a poisoned memory entry is a backdoor that reloads every session.) The 900+ agent gateways found exposed on the public internet in early 2026 — plaintext credentials, no authentication — failed precisely because there was no governance layer recording and enforcing any of this. Audit is the floor of that governance layer.

The trifecta audit: a gate before production

The lethal trifecta becomes a concrete pre-deployment gate. Before any new agent ships, audit it explicitly:

TRIFECTA AUDIT (run before every agent goes to production)
[ ] Does it access private data? (repo, secrets, DB, customer data)
[ ] Does it process untrusted content? (issues, PRs, web, dependencies)
[ ] Can it communicate externally? (network, public comments, APIs)

All three present? → DO NOT SHIP without compensating controls:
• break it with the Rule of Two (split read-only from write-capable), OR
• content inspection + strict tool scoping + mandatory human gates on egress
Assign an explicit owner to each leg. "Logging" is not "prevention."

Make this audit a required, version-controlled step — not tribal knowledge. Only 11% of production agents passed a baseline security assessment in 2026, and 57% of organizations couldn’t even answer these three questions for their agents. Being able to answer them, on every agent, before it ships, already puts you ahead of the overwhelming majority.

Govern from day one

Tie it together with a governance layer that exists from the first commit, not after the first incident. Version-control your agent security policy and risk map alongside the code; align your controls to a recognised framework — the OWASP Top 10 for Agentic Applications 2026, MITRE ATLAS for adversary techniques, and the NIST AI Risk Management Framework for lifecycle governance — so you’re not inventing controls from scratch; and gate the installation of new tools and plugins behind review (CODEOWNERS plus CI schema validation), since a new tool is a new capability and a new piece of attack surface. Governance isn’t a launch-day checklist; it’s the substrate the agent runs on.

The whole picture

Assemble the three parts and the architecture is coherent. You secure the harness, not the model, because the model can be compromised — and a coding agent’s blast radius at machine speed makes that compromise expensive. The harness is defence in depth organised by the Rule of Two — least privilege, ephemeral sandboxing, default-deny egress, a credential broker, and a policy gateway that doesn’t trust the model’s reasoning. Security lives in the workflow — deterministic gates on every PR, risk-tiered human approval, decision-level audit, and a trifecta gate before production — all sitting on a governance layer present from day one.

The thread through every layer is the same: a single compromised agent should stay a single bad session, never a production-wide incident. You get there not by trusting the agent more, but by engineering the workflow so that trust is never required — the controls run whether the agent is behaving or not. That’s what “built into the workflow, not bolted on” actually means, and at 10 vulnerabilities a week, it’s the only thing that keeps pace.

Agentic Engineering: From Vibe Coding to a Plan, Execute, Verify Discipline

For about a year, “vibe coding” was the most fun anyone had had with a keyboard. You describe what you want, an agent writes it, you skim the result, you ship. Andrej Karpathy’s original framing was almost gleeful: you “give in to the vibes” and barely read the diffs.

Then teams tried to put vibe-coded software into production, and the bill came due.

The failure mode now has a name — AI slop: code that looks reasonable on the surface but lacks error handling, quietly introduces security vulnerabilities, breaks something three modules over, or produces an architecture nobody can maintain. It’s not that the model is dumb. It’s that “prompt and hope” has no step where anything gets checked against reality before it lands.

The numbers make the discomfort concrete. In Sonar’s 2026 developer survey, 96% of developers said they don’t fully trust the output of AI coding agents. A late-2025 Stack Overflow survey found nearly half of developers were frustrated by AI solutions that are “almost right, but not quite” — which is arguably the most expensive kind of wrong, because it survives a casual review and fails in production.

In early 2026, Karpathy named the thing that comes next: agentic engineering — the discipline of designing systems where AI agents plan, write, test, and ship code under structured human oversight. Not casual prompting. Not hope-and-check. An actual engineering methodology built for AI-first development.

This series is a hands-on guide to that discipline. By the end you’ll have built a loop you’d actually trust near your codebase.

What vibe coding is missing

Strip a vibe-coding session down and you get:

You: "Add rate limiting to the API"
Agent: <writes 80 lines>
You: <skim, looks fine, paste it in>

Three things are absent, and each is a place production breaks:

  1. No explicit plan. The agent inferred intent and committed to an approach in a single shot. You never saw the approach, so you couldn’t catch that it chose an in-memory limiter that won’t survive a multi-instance deploy.
  2. No isolation. The code went straight onto your working branch. When it’s wrong, untangling it from your own changes is a chore.
  3. No verification gate. “Looks fine” is the only test. There’s no point where tests, a linter, or a security scanner can block the change before it reaches you.

Agentic engineering reintroduces all three as first-class steps. The core workflow that replaces “prompt and hope” is the Plan → Execute → Verify loop, usually shortened to PEV.

The PEV loop

Flowchart illustrating the software development process with four stages: Plan, Execute, Verify, and Ship, emphasising human oversight and feedback loops for failure.

Each phase has a job:

  • Plan turns a fuzzy request into an explicit, reviewable artifact: a spec and a task breakdown. This is where a human can intervene cheaply — catching a bad approach before a line of code exists costs seconds; catching it after costs a debugging session.
  • Execute writes the code, but in an isolated environment (a fresh branch, a sandboxed workspace) so a bad run can be thrown away with zero blast radius.
  • Verify runs objective checks — the test suite, linters, type checks, security scanners — and either passes the work forward or kicks it back to Plan with the failure as new context.

The arrow wrapping the whole thing is the part people skip and shouldn’t: human oversight. The human isn’t in the inner loop typing code. They’re at the boundaries — approving the plan, adjudicating verify failures the agent can’t resolve, and owning the final merge decision.

That distinction — humans at the boundaries, not in the middle — is the whole philosophy. It’s what lets the loop run fast without running blind.

A minimal PEV loop you can run

Let’s make this concrete with the smallest thing that’s still genuinely a PEV loop. We’ll give an agent a tiny task, force it to plan first, let it execute, then actually verify with a real test run instead of vibes.

We’ll use Python and the Anthropic Messages API, but nothing here is provider-specific — swap in any model SDK.

import subprocess, json, tempfile, os
from anthropic import Anthropic

client = Anthropic()
MODEL = "claude-sonnet-4-6"

def call(system, user):
msg = client.messages.create(
model=MODEL, max_tokens=2000,
system=system,
messages=[{"role": "user", "content": user}],
)
return "".join(b.text for b in msg.content if b.type == "text")

# ---- PLAN ---------------------------------------------------------------
def plan(task):
system = (
"You are a senior engineer. Given a task, produce a short, explicit "
"plan as JSON with keys: 'approach' (one sentence) and 'steps' (list). "
"Do not write code yet. Output JSON only, no prose, no code fences."
)
return json.loads(call(system, task))

# ---- EXECUTE ------------------------------------------------------------
def execute(task, the_plan):
system = (
"You are a careful engineer. Implement the task following the plan. "
"Return JSON only with keys 'code' (the module) and 'tests' (pytest "
"tests for it). No prose, no code fences."
)
user = f"TASK:\n{task}\n\nPLAN:\n{json.dumps(the_plan, indent=2)}"
return json.loads(call(system, user))

# ---- VERIFY -------------------------------------------------------------
def verify(artifact):
"""Run the agent's tests in an isolated temp dir. Real check, not vibes."""
with tempfile.TemporaryDirectory() as d:
open(os.path.join(d, "solution.py"), "w").write(artifact["code"])
open(os.path.join(d, "test_solution.py"), "w").write(artifact["tests"])
result = subprocess.run(
["python", "-m", "pytest", "-q"],
cwd=d, capture_output=True, text=True, timeout=60,
)
return result.returncode == 0, result.stdout + result.stderr

# ---- THE LOOP -----------------------------------------------------------
def pev(task, max_attempts=3):
feedback = ""
for attempt in range(1, max_attempts + 1):
print(f"\n=== Attempt {attempt} ===")
the_plan = plan(task + feedback)
print("PLAN:", the_plan["approach"])

artifact = execute(task, the_plan)
passed, log = verify(artifact)

if passed:
print("VERIFY: passed ✅ (awaiting human review before merge)")
return artifact
print("VERIFY: failed ❌ — replanning with the failure as context")
feedback = f"\n\nPrevious attempt failed these tests:\n{log[-1500:]}"

print("Gave up after max attempts — escalate to a human.")
return None

if __name__ == "__main__":
pev("Write a function `slugify(s)` that lowercases, strips punctuation, "
"and replaces runs of whitespace with single hyphens.")

Run it and watch the difference from vibe coding: the agent is forced to state an approach before coding, the work runs in a throwaable directory, and the loop only declares success when a real pytest process exits zero. A failure doesn’t get shipped — it gets fed back in as context for the next plan.

What this toy is still missing

This is a real loop, but it’s a toy, and the gaps are exactly the subject of later sections:

  • The plan is unreviewed. A human should be able to approve or edit it before execution.
  • Verification is shallow. Passing the agent’s own tests proves very little — the agent can write weak tests. We need independent tests, linters, type checks, and security scanning.
  • No real isolation. A temp dir works for one function; real work needs branch-per-task and git worktrees so parallel runs don’t collide.
  • One agent does everything. Author and grader being the same model is a conflict of interest. Splitting roles — author, tester, reviewer, security — catches far more.
  • No audit trail. In production, why an agent did something becomes the constraint, not whether it could.

Building a production grade PEV loop

Above, we built a PEV loop that ran an agent’s own tests in a temp directory. It was a real loop but a weak one: the plan was unreviewed, the verification trusted the agent to grade itself, and “isolation” was a folder that vanished. We’ll build each phase properly: a plan you can read and approve, an execute phase that runs in a real isolated workspace, and a verify gate the agent cannot talk its way past.

Phase 1 — Plan: make intent explicit and reviewable

The single highest-leverage move in agentic engineering is forcing a planning step before any code exists. A bad approach caught at the plan stage costs seconds. The same mistake caught after execution costs a debugging session — and after merge, an incident.

A good plan is a structured artifact, not a paragraph. Structure makes it reviewable, diff-able, and machine-checkable. A practical shape:

from dataclasses import dataclass, field
from typing import Literal

@dataclass
class PlanStep:
id: str
description: str
files_touched: list[str]
risk: Literal["low", "medium", "high"]

@dataclass
class Plan:
goal: str
approach: str # one sentence the human can sanity-check
non_goals: list[str] # what we are deliberately NOT doing
steps: list[PlanStep]
acceptance_criteria: list[str] # the verify phase will check these
open_questions: list[str] = field(default_factory=list)

Two fields earn their keep. non_goals stops scope creep — the most common way an agent turns “add rate limiting” into a rewrite of your middleware stack. acceptance_criteria is the contract the Verify phase will hold the code to; writing it during planning means “done” is defined before a line is written, not rationalized afterward.

The prompt that produces this should be explicit about the spec-first stance:

PLAN_SYSTEM = """You are a staff engineer doing spec-driven development.
Given a task and the relevant repo context, produce a Plan object as JSON.
Rules:
- Decompose into the smallest steps that each leave the repo green.
- State non_goals explicitly to bound scope.
- Write acceptance_criteria as concrete, testable statements.
- If the task is ambiguous, populate open_questions instead of guessing.
Output JSON only."""

Notice the last rule. A vibe-coding agent guesses when it’s unsure and you find out later. A planning agent is told to surface ambiguity as open_questions — which becomes the natural place for a human to intervene. If open_questions is non-empty, the loop pauses for an answer instead of charging ahead on an assumption.

Phase 2 — Execute: isolation is non-negotiable

The reason vibe coding feels dangerous is that the agent writes directly onto your working tree. Get isolation right and a botched run becomes a deleted branch instead of an afternoon of git reset archaeology.

The right primitive is one git worktree per task. A worktree gives each agent run its own checked-out branch in its own directory, all backed by the same repo — so parallel agents can work without colliding, and merging back is an ordinary PR.

import subprocess, uuid, pathlib

def make_worktree(repo: str, base: str = "main") -> tuple[str, str]:
"""Create an isolated worktree on a fresh branch. Returns (path, branch)."""
branch = f"agent/{uuid.uuid4().hex[:8]}"
path = pathlib.Path(repo).parent / "worktrees" / branch.replace("/", "-")
subprocess.run(["git", "-C", repo, "worktree", "add", "-b", branch,
str(path), base], check=True)
return str(path), branch

def teardown_worktree(repo: str, path: str, branch: str, keep: bool):
if not keep:
subprocess.run(["git", "-C", repo, "worktree", "remove", "--force", path])
subprocess.run(["git", "-C", repo, "branch", "-D", branch])

Now execution happens inside that directory. The agent gets tools scoped to the worktree — read a file, write a file, run a shell command — and nothing it does can touch your main checkout. This is also where you cap blast radius with the principle of least privilege: the execution sandbox should have only the filesystem, network, and credential access the task genuinely needs. An agent implementing slugify does not need your production database URL in its environment.

def execute_in_worktree(client, task, plan, wt_path):
"""Agentic execution with file + shell tools, confined to wt_path."""
tools = [
{"name": "write_file", "description": "Write a file (path relative to repo root).",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"}, "content": {"type": "string"}},
"required": ["path", "content"]}},
{"name": "run", "description": "Run a shell command in the repo root.",
"input_schema": {"type": "object",
"properties": {"cmd": {"type": "string"}}, "required": ["cmd"]}},
]
# ... a standard tool-use loop: call the model, dispatch tool calls against
# wt_path with subprocess(cwd=wt_path), feed results back until the model
# signals completion. Every shell command is confined to the worktree.

The implementation detail that matters: every tool call executes with cwd=wt_path, and the write_file handler rejects any path that resolves outside the worktree (guard against ../ escapes). Isolation you can bypass isn’t isolation.

Phase 3 — Verify: the gate the agent can’t sweet-talk

This is where the previous loop was weakest. Letting the agent write and grade its own work is a conflict of interest — it’ll write tests that pass. Production verification needs to be objective and independent of the author, and it needs multiple lenses, because each catches a different class of slop.

Think of the gate as a pipeline of checks, each of which can block:

@dataclass
class Check:
name: str
cmd: list[str]
blocking: bool = True

GATE = [
Check("format", ["ruff", "format", "--check", "."]),
Check("lint", ["ruff", "check", "."]),
Check("types", ["mypy", "."]),
Check("tests", ["pytest", "-q", "--cov", "--cov-fail-under=80"]),
Check("security", ["bandit", "-r", ".", "-ll"]), # catches injected vulns
Check("deps", ["pip-audit"]), # known-CVE dependencies
]

def verify(wt_path) -> tuple[bool, dict]:
results, ok = {}, True
for c in GATE:
r = subprocess.run(c.cmd, cwd=wt_path, capture_output=True,
text=True, timeout=300)
passed = r.returncode == 0
results[c.name] = {"passed": passed, "log": (r.stdout + r.stderr)[-2000:]}
if c.blocking and not passed:
ok = False
return ok, results

A few deliberate choices:

  • A coverage floor (--cov-fail-under=80) stops the agent from “passing” by writing one trivial test. It has to actually exercise the code.
  • A security scanner (bandit) and a dependency auditor (pip-audit) are not optional niceties. As we’ll see later, an agent producing code at volume produces vulnerabilities at volume unless something blocks them. Security belongs in the gate, not in a later review.
  • Independent tests matter. A strong setup has a second agent (or a human-owned test suite) write tests the author agent never sees. Self-graded tests are a starting point, not the finish line.

Wiring it together

def production_pev(repo, task, max_attempts=3):
feedback = ""
for attempt in range(1, max_attempts + 1):
plan = make_plan(task + feedback) # Phase 1
if plan.open_questions:
return pause_for_human(plan) # don't guess — escalate

wt_path, branch = make_worktree(repo) # Phase 2: isolate
try:
execute_in_worktree(client, task, plan, wt_path)
ok, results = verify(wt_path) # Phase 3: gate
if ok:
open_pull_request(branch, plan, results) # human merges
teardown_worktree(repo, wt_path, branch, keep=True)
return branch
# failed gate → feed the specific failures back into the next plan
feedback = summarize_failures(results)
finally:
teardown_worktree(repo, wt_path, branch, keep=ok)
escalate_to_human(task, feedback)

The shape is the same loop from earlier, but every phase now has teeth: the plan is a reviewable artifact that refuses to guess, execution is confined to a disposable worktree with least-privilege access, and verification is an independent multi-check gate with a coverage floor and security scanning. Crucially, a failure doesn’t ship — it becomes precise feedback (summarize_failures extracts the actual failing test names and scanner findings) that sharpens the next plan.

What’s still open

We now have a loop that’s safe to point at a real repo. But two things still rely on hand-waving:

  1. “Human merges” and “escalate to a human” — we keep deferring to a human at the boundaries without saying how that handoff should work. Where exactly do humans belong, and how do you keep them effective without making them a bottleneck?
  2. One agent still does the work. We hinted that independent test-writing helps. The full version splits the job across specialized agents — author, tester, reviewer, security — and that orchestration is its own design problem.

Human Oversight, Multi-Agent Orchestration & Shipping Safely

We have a loop that plans explicitly, executes in isolation, and verifies objectively. Left alone, though, it has two unresolved weaknesses: it still treats “a human handles it” as a magic step, and it still has one agent doing — and grading — all the work. Fixing both is what separates a demo from something you’d run against your main branch a hundred times a day.

Where humans actually belong

The initial instinct is to put a human “in the loop.” That’s the wrong picture. A human reviewing every diff an agent produces is just slow vibe coding — you’ve added a bottleneck without adding rigor, and reviewers rubber-stamp under volume anyway.

The right model is humans at the boundaries, agents in the loop. There are exactly three boundaries worth a human’s attention:

  1. Plan approval (the cheap gate). Reviewing a one-paragraph approach plus non_goals and acceptance_criteria takes thirty seconds and catches the most expensive mistakes — wrong approach, wrong scope — before any code exists. This is the single highest-ROI place to spend human attention. Pair it with the open_questions mechanism from earlier: if the agent is unsure, it asks here instead of guessing.

  2. Verify escalation (the exception gate). When the loop exhausts its attempts or hits a failure it can’t resolve, a human adjudicates. The key design rule: the human should receive the structured failure — which checks failed, the actual scanner findings, what the agent already tried — not a raw transcript. Make the escalation legible and it takes a minute; dump a 4,000-line log and it takes an hour.

  3. The merge decision (the accountability gate). A passing verify gate produces a pull request, not a merge. A human owns the decision to land it. This isn’t ceremony — it’s where accountability lives. You can’t fire a bot; someone human is answerable for what reached production.

PullRequest = {
"branch": branch,
"plan": plan, # the approved approach + acceptance criteria
"gate_results": results, # every check, pass/fail, with logs
"diff_stat": diff_summary, # what actually changed
"agent_trail": run_id, # link to the full audit trail (see below)
}

Everything else — the writing, the testing, the iterating — happens without a human in the inner loop. That’s what lets the system move fast. The boundaries are where speed and safety get reconciled.

Multi-agent orchestration: stop letting the author grade itself

Earlier, we flagged the conflict of interest in one agent writing and testing its own code. The production answer is to split the job into specialized roles, each a focused agent with its own prompt, its own context, and — importantly — no incentive to cover for the others.

A battle-tested division of labor:

  • Planner owns the spec (Phase 1). It does not write code.
  • Author implements against the plan. It does not write its own acceptance tests.
  • Tester writes tests from the plan’s acceptance criteria, not from the author’s code — so the tests check intended behavior, not whatever the author happened to build. This single separation kills a huge fraction of “passes its own tests, fails in prod” slop.
  • Reviewer reads code + tests against the plan and can reject back to Planner with reasons. It’s looking for the things scanners miss: bad architecture, missing edge cases, misread requirements.
  • Security runs as its own role and as part of the automated gate. It looks specifically for injected vulnerabilities, secrets, and unsafe dependencies.

You don’t need heavyweight frameworks to start — each role is a function that calls a model with a role-specific system prompt and passes a structured artifact to the next. Orchestration can be a plain state machine. Reach for an agent framework only when you actually need durable state, parallelism, or cross-process coordination; premature orchestration infrastructure is its own kind of slop.

The security math you can’t argue with

Here’s the calculation that makes everything above non-negotiable rather than nice-to-have. Anthropic’s 2026 agentic coding guidance puts it bluntly:

An agent producing 1,000 pull requests a week at a 1% vulnerability rate ships 10 new vulnerabilities every week.

Manual review cannot keep pace with that — which is the whole point. The same scaling that makes agentic engineering powerful for you makes it powerful for an attacker, and it makes your own agents a vulnerability factory unless something blocks bad output automatically.

Three consequences:

  1. Security lives in the harness, not in a later review. Every PEV cycle runs security scanning as a blocking check (gate). Bolting it on afterward means you’ve already shipped the slop.
  2. Least privilege is structural. Each execution sandbox gets only the filesystem, network, and credentials its task needs. An agent’s expanded attack surface — it touches APIs, databases, external services — is exactly what a scoped sandbox contains.
  3. New attack classes are real. “Living off the agent” — hijacking an enterprise AI’s own permissions to act maliciously — is an emerging 2026 tactic. Treat the agent’s credentials and tool access as a primary attack surface, not plumbing.

Auditability is the real constraint

There’s a counterintuitive lesson from teams running this at scale: the bottleneck stops being whether agents can do the work and becomes whether you can account for what they did. As agentic dev tooling has boomed through 2026, workflow auditability has become the binding constraint.

Every run should emit an immutable trail: the task, the approved plan, which agent did what, every tool call and its result, the full gate output, and who approved the merge. This isn’t bureaucracy — it’s what makes incidents debuggable, makes compliance possible in regulated environments, and makes the merge gate meaningful (a human approving a PR needs to be able to see why the agent did what it did).

def emit_trail(run_id, event, payload):
record = {"run_id": run_id, "ts": now_iso(), "event": event, "payload": payload}
append_only_log.write(record) # tamper-evident, queryable, retained

If you build one thing beyond the loop itself, build this. The teams that succeed with agentic engineering aren’t the ones with the cleverest agents; they’re the ones who can answer “what happened and why” for any change that reached production.

Anti-patterns to avoid

A few failure modes show up repeatedly:

  • Automating a broken process. Gartner projects that ~40% of agentic projects will be cancelled by 2027 — largely not because the tech fails, but because teams automate workflows that were already broken. PEV makes a bad process faster, not better. Fix the process first.
  • Human-in-the-inner-loop. Reviewing every diff doesn’t scale and trains reviewers to rubber-stamp. Move humans to the three boundaries.
  • Self-graded work. If the author writes the tests, you’re measuring the author’s confidence, not correctness. Separate the roles.
  • Optional security. At agent volume, “we’ll add scanning later” means shipping vulnerabilities now.
  • Premature orchestration. Don’t reach for a multi-agent framework on day one. Start with a single PEV loop and one human boundary; add roles when a specific failure mode demands one.

A production-readiness checklist

Before you point an agentic loop at a repo that matters:

  • [ ] Planning produces a reviewable, structured spec with explicit non_goals and acceptance_criteria.
  • [ ] The agent surfaces ambiguity as questions instead of guessing.
  • [ ] Every run executes in an isolated, disposable worktree.
  • [ ] The execution sandbox has least-privilege filesystem/network/credential access.
  • [ ] The verify gate is independent of the author and includes tests with a coverage floor, type checks, linting, security scanning, and dependency auditing.
  • [ ] Tests are written from acceptance criteria, not from the author’s code.
  • [ ] Humans sit at exactly three boundaries: plan approval, verify escalation, merge.
  • [ ] A passing gate produces a PR, never an auto-merge.
  • [ ] Every run emits an immutable, queryable audit trail.
  • [ ] You can answer “what happened and why” for any agent-made change.

Where this leaves you

Agentic engineering isn’t about replacing developers — it’s about multiplying what one developer can responsibly oversee. The teams pulling ahead aren’t the ones who let agents run wildest; they’re the ones who turned “prompt and hope” into Plan → Execute → Verify, kept humans at the boundaries where their judgment compounds, and made every run accountable.

That’s the whole discipline. The agents will keep getting better. The engineering — the plan, the isolation, the gate, the oversight, the trail — is the part that’s yours, and it’s the part that decides whether all that capability ships value or ships slop.

Eyes Everywhere: Secure Logging and Alerting for Modern Systems – Part III


Logging and alerting become truly powerful only when they are embedded into a well-designed security architecture. Individual applications emitting logs or isolated detection rules provide only partial visibility. Modern organisations operate distributed systems composed of microservices, cloud infrastructure, container platforms, and external APIs. In such environments, security visibility requires a unified architecture capable of collecting, analysing, and responding to telemetry across the entire system.


Designing Systems That Detect and Respond to Threats

A secure observability architecture combines several layers. Applications generate structured logs. Infrastructure produces telemetry about hosts, containers, and network activity. These signals are collected and aggregated through centralized pipelines, analyzed by security analytics platforms, and ultimately transformed into alerts and automated responses.

Application Layer Logging

Application logs represent the most detailed source of telemetry because they capture business logic events. These events include authentication attempts, financial transactions, configuration changes, and access to sensitive resources.

Applications should emit structured logs that can be easily parsed by downstream analytics systems. A simple implementation using Python might produce JSON-formatted events.

import json
import logging
from datetime import datetime

logger = logging.getLogger("app")

def log_user_action(user_id, action, resource):

event = {
"timestamp": datetime.utcnow().isoformat(),
"event_type": "user_action",
"user_id": user_id,
"action": action,
"resource": resource,
"service": "account-service"
}

logger.info(json.dumps(event))

Such structured events allow centralized systems to search and correlate activity across multiple services. A user accessing a resource through an API gateway, for example, may generate logs in multiple backend services. A consistent schema allows analysts to reconstruct the entire request path.

Application logs also provide the richest context for security detection because they capture events at the business logic level rather than merely technical operations.

Infrastructure Telemetry

While application logs capture internal logic, infrastructure telemetry reveals the behavior of the underlying environment. Systems must monitor operating systems, network activity, and runtime platforms to detect suspicious behavior that occurs outside the application layer.

Operating systems produce authentication logs, process execution records, and network activity events. On a Linux system, failed SSH login attempts may appear in the authentication log.

Failed password for invalid user root from 203.0.113.11 port 42122 ssh2

Infrastructure monitoring agents collect these events and forward them to centralized logging systems.

A simple agent configuration might stream logs to a central collector.

filebeat.inputs:
- type: log
paths:
- /var/log/auth.log

output.elasticsearch:
hosts: ["https://log-server.example.com:9200"]

Network telemetry is also crucial. Firewall and network gateway logs reveal scanning attempts, suspicious connections, and unusual traffic flows.

An example network connection event might appear as structured telemetry.

{
"event_type": "network_connection",
"source_ip": "198.51.100.44",
"destination_port": 22,
"protocol": "TCP",
"timestamp": "2026-06-01T13:42:18Z"
}

Combining application and infrastructure telemetry provides comprehensive security visibility.

Aggregation Layer

Large systems generate logs from thousands of sources. Without aggregation, these logs remain scattered across machines and services.

The aggregation layer collects logs from applications, infrastructure components, and network devices. Log collectors such as Fluent Bit, Logstash, or Vector ingest telemetry and forward it to centralized platforms.

A typical log collection configuration might look like the following Fluent Bit pipeline.

fluent-bit \
-i tail \
-p path=/var/log/app.log \
-o http \
-p host=logging.example.com \
-p port=8080

Collectors normalize log formats and enrich events with metadata such as host identifiers, container IDs, or geographic location. This enrichment improves searchability and correlation across systems.

Aggregation pipelines also buffer logs during network disruptions to ensure no data is lost.

Analysis Layer

Once collected, logs must be analyzed to detect suspicious behavior. This is the role of the analysis layer.

Security analytics platforms ingest aggregated telemetry and apply detection rules, statistical models, and anomaly detection algorithms.

A SIEM platform might evaluate login activity using a query such as the following.

SELECT username, COUNT(*) AS failures
FROM logs
WHERE event_type = 'authentication_failure'
AND timestamp > NOW() - INTERVAL '10 minutes'
GROUP BY username
HAVING COUNT(*) > 20

If this query produces results, the platform triggers an alert.

More advanced analytics engines also perform behavioral analysis. These systems learn typical usage patterns and detect deviations.

An anomaly detection algorithm implemented in Python might evaluate whether a user’s activity deviates significantly from historical behavior.

def detect_anomaly(current_activity, baseline):
threshold = baseline * 5
if current_activity > threshold:
return True
return False

These analytic processes convert massive volumes of telemetry into actionable intelligence.

Response Layer

Detection alone is insufficient. Systems must also respond to threats.

The response layer integrates alerting systems with operational tools such as incident management platforms, messaging systems, and automated security controls.

When an alert is triggered, the system may send notifications to security teams.

def send_alert(message):
alert = {
"alert_type": "security_event",
"message": message,
"timestamp": datetime.utcnow().isoformat()
}
notify_security_team(alert)

Automated responses may also be triggered for critical threats. For example, a brute-force attack might result in immediate blocking of the source IP address.

def block_ip(ip_address):
firewall.block(ip_address)

These automated defenses reduce response time and limit the impact of attacks.

Securing the Logging Pipeline

Protecting Log Transport

Log data often travels across networks before reaching centralized systems. If this communication is not secured, attackers could intercept or manipulate logs.

Secure logging pipelines encrypt log transport using TLS.

A log collector configuration may enforce encrypted transport.

output:
elasticsearch:
hosts: ["https://logs.example.com:9200"]
ssl.certificate_authorities: ["/etc/certs/ca.pem"]

TLS ensures confidentiality and prevents unauthorized interception of telemetry.

Mutual authentication can also verify that only trusted systems send logs to the platform.

Preventing Log Tampering

Attackers frequently attempt to erase or modify logs to hide evidence of their activity.

To protect against this, logging systems implement append-only storage and integrity verification.

One approach uses cryptographic hashing to chain log entries together.

import hashlib

def generate_log_hash(entry, previous_hash):
combined = entry + previous_hash
return hashlib.sha256(combined.encode()).hexdigest()

Each log entry includes the hash of the previous entry. If an attacker modifies an entry, the hash chain becomes invalid, revealing the tampering.

Write-once storage systems further protect logs by preventing modification after ingestion.

Isolation of Logging Infrastructure

Security telemetry must be isolated from application environments. If attackers gain access to the same infrastructure that stores logs, they may attempt to manipulate or delete evidence.

Organizations often deploy logging infrastructure in dedicated environments accessible only to security teams.

A simplified architecture may route logs from production systems to a separate security network.

app_server -> log_collector -> security_logging_cluster

Strict access control policies ensure that application administrators cannot modify stored logs.

Isolation ensures the integrity of forensic data during incident investigations.

Cloud-Native Logging and Alerting

Observability in Microservices Architectures

Microservices architectures introduce new challenges for observability. A single user request may traverse dozens of services before completing.

Distributed tracing provides visibility into these interactions by assigning correlation identifiers to requests.

A service might attach a request ID to every log entry.

const requestId = generateRequestId();

logger.info({
request_id: requestId,
event: "api_request_received"
});

Downstream services propagate this identifier so analysts can trace the entire execution path.

Kubernetes and Container Logs

Container orchestration platforms generate extensive telemetry about container lifecycles, pod scheduling, and cluster activity.

Kubernetes exposes logs through its API.

kubectl logs deployment/payment-service

Security-relevant cluster events may include pod creation, container crashes, and unexpected resource modifications.

A cluster event might look like the following.

{
"event_type": "pod_created",
"namespace": "production",
"pod_name": "api-server-6f45",
"timestamp": "2026-06-01T14:22:33Z"
}

Monitoring these events allows organizations to detect suspicious deployments or unauthorized configuration changes.

Serverless Logging Challenges

Serverless environments introduce additional complexity because execution environments are ephemeral. Functions may run for only a few milliseconds before terminating.

As a result, logs must be exported immediately to centralized systems.

A serverless function might log activity using a cloud-native logging service.

import logging

def handler(event, context):
logging.info({
"event_type": "function_execution",
"function": "payment_handler",
"timestamp": context.timestamp
})

Centralized logging ensures that transient environments do not lose telemetry.

Automation and AI in Security Monitoring

Automated Detection Pipelines

Security monitoring increasingly relies on automated detection pipelines capable of processing large volumes of telemetry in real time.

Machine learning models can identify patterns that traditional rule-based systems might miss.

A simple anomaly detection model might analyze login frequency.

from sklearn.ensemble import IsolationForest

model = IsolationForest()
model.fit(training_data)
prediction = model.predict(new_login_data)
if prediction == -1:
trigger_alert("Login anomaly detected")

Such models can detect subtle deviations in user behavior.

AI-Assisted Threat Analysis

Artificial intelligence systems can assist analysts by prioritizing alerts and correlating events across large datasets.

An AI system might analyze multiple signals simultaneously.

if unusual_login and new_device and large_data_access:
alert("Possible account compromise")

By correlating signals across logs, AI systems can identify complex attack patterns.

Automated Response

Automation can also mitigate attacks automatically.

For example, if a system detects repeated authentication failures from a specific IP address, it may block that address.

if failed_attempts > 50:
firewall.block(source_ip)

If suspicious activity occurs on a user account, automated controls may disable the account temporarily.

def disable_account(user_id):
account_service.disable(user_id)

Automation reduces the time between detection and response, limiting the damage attackers can cause.

Logging and Alerting Best Practices Checklist

Principles for Secure Observability

Effective observability systems focus on collecting meaningful telemetry, protecting the integrity of logs, and designing alerts that enable rapid response.

Applications should log security-relevant events using structured formats. Infrastructure telemetry must complement application logs to provide full visibility into system behavior. Detection rules must evolve continuously as new threats emerge.

Security monitoring is not a static system but an evolving process.

Continuous Improvement

Attackers constantly change tactics, techniques, and procedures. Security monitoring systems must therefore evolve continuously.

Detection rules should be refined based on real-world incidents and emerging threat intelligence.

Security teams often review historical incidents and update detection logic accordingly.

def update_detection_rules(new_patterns):
rules_engine.add(new_patterns)

This iterative improvement ensures that monitoring systems remain effective against modern threats.

Observability as a Security Strategy

Modern software systems produce enormous volumes of telemetry. Yet data alone does not provide protection. Security emerges only when organizations design observability systems that transform telemetry into actionable intelligence.

Secure logging provides the foundation. Alerting transforms logs into early warning signals. Automated responses limit the impact of attacks.

When implemented correctly, logging and alerting systems become the digital equivalent of surveillance infrastructure, continuously monitoring the environment for suspicious activity.

In an era where attackers move rapidly and stealthily, secure observability gives defenders the visibility required to detect threats early, investigate incidents effectively, and protect critical systems before damage occurs.