Vercel Logo

Approval Config

You already have an approval system. Module 2 built the discriminated union with interactive, background, and delegated modes. That answers one question: who decides?

It doesn't answer the other one: what specific policies apply?

A CI run uses mode: "background" to auto-approve commands. Fine. But you also want to block any write to .env, regardless of mode. You want bash commands wrapped in a stricter OS-level sandbox, regardless of mode. Those rules don't fit the discriminated union. They live one layer down.

This lesson sets up the second layer (event-based interception) and shows where the two models meet. The event layer is conceptual in the build-along, but the shape is worth seeing now so you can wire it in when you need it.

Outcome

You can describe two approval models, identify which one fits which use case, and explain how they combine in production harnesses.

The Two Models

The config model is what you have:

type ApprovalConfig =
  | { mode: "interactive" }
  | { mode: "background" }
  | { mode: "delegated"; trust: string[] };

Set at startup. Doesn't change during a session. Answers who decides.

The event model is the layer below:

src/approval-events.ts (sketch)
harness.on("tool_call", async (event) => {
  const { toolName, input } = event;
 
  if (toolName === "write" && input.path.endsWith(".env")) {
    return { block: true, reason: "Cannot modify .env files" };
  }
 
  if (toolName === "bash") {
    event.input.command = `sandbox-exec -p '(deny default)' ${input.command}`;
  }
 
  return { block: false };
});

Fires on every tool call. Extensions can block, modify, or pass through. Answers what policies apply.

When to Use Which

Use caseConfigEvents
CI run, auto-approve everythingmode: "background"Overkill
Subagent inheriting trust from parentmode: "delegated"Wrong level
Block writes to specific filesToo coarseFile-level policy
Wrap commands in OS-level sandboxCan't modify inputInput modification
Project-specific safety rulesGlobal onlyPer-project extension

The config layer is for the operational mode of a whole session. The event layer is for fine-grained, often project-specific, often pluggable policies. They overlap a little. They don't replace each other.

How They Combine

A real harness uses both:

src/index.ts (sketch)
const approval = createApproval({ mode: "interactive" });
 
harness.on("tool_call", async (event) => {
  if (event.toolName === "write" && event.input.path.endsWith(".env")) {
    return { block: true, reason: "Protected file" };
  }
});

The config says "interactive mode, the human approves." The event handler says "regardless of what the human approves, never touch .env." The event fires after the config but before the tool runs. Defense in depth.

This matters because operational modes and policies tend to come from different places. The mode is set by who's running the harness (CI, a developer, a delegated subagent). The policies are set by the project (.env is sensitive, the build directory is read-only, anything that touches production credentials needs OS-level sandboxing). One config knob can't carry both kinds of decisions without getting tangled.

The event layer slots into Module 11

We'll build the actual event bus in Module 11's extensibility work, where lifecycle events are the primary extension point. Approval events are one specific kind of lifecycle event. Once the bus exists, the approval interceptor is a few lines of code that subscribes to tool_call.

What's Missing in the Build-Along

The harness you've built so far has the config layer. It doesn't have an event layer yet. That's fine. The config layer covers most of the cases the course needs to teach.

Adding the event layer would look like this:

  1. Build a small typed event emitter in the harness (Module 11)
  2. Emit a tool_call event from the agent loop before each tool runs
  3. Let subscribers return { block, reason } or modify the input
  4. Wire one subscriber that blocks writes to a hardcoded file as a smoke test

The work is small. It's not in this module because the prerequisites (events, extensions) belong with the rest of the extensibility story. When you reach Module 11, the event-based approval becomes a single concrete example of why the event bus is useful.

Try It

This is a concept lesson. Check yourself:

  1. For each of the five use cases in the table above, decide which model fits best
  2. Sketch a single use case where both models apply at once. What does each layer do?
  3. Identify a project you've worked on where event-based approval would have caught a real mistake. What would the rule have been?

Commit

No code in this lesson. The event layer arrives in Module 11.

Done-When

  • You can describe the config approach and what it answers
  • You can describe the event approach and what it answers
  • You can pick which one fits a given use case
  • You can sketch how the two combine for defense in depth
Build a risk-scored auto-approval

Binary approve and deny is crude. Try a riskScore(command) function that returns a number from 0 to 100. Score factors: writes to disk add 30, network access adds 20, file deletion adds 50, modifying config adds 40, read-only is 0. Set a threshold of, say, 40. Below auto-approves. Above prompts the user. Log every auto-approval with its score so you can audit later. Add a --risk-threshold flag so users can tune their own comfort level. Now figure out how to score rm -rf /tmp/test differently from rm -rf / without scoring purely on keywords.

Was this helpful?

supported.