The Drift Log · Governing agents that write code
Enforce schema contracts for agent‑generated code at merge time
13 September 2026 · 3 min read · 701 words · established

A deterministic, model‑independent validator can block non‑conforming agent code at merge time, keeping the main branch clean.
The failure: unchecked agent code reaches the main branch
Agents can generate TypeScript files, push them, and the CI system will merge them without a human eye. The result is code that does not respect the project's public API, violates naming conventions, or introduces subtle type mismatches. When the breakage surfaces weeks later, the cost of a hot‑fix dwarfs the time saved by the agent. The root cause is the absence of a code gate that validates the agent's output against the project's schema contracts before the merge is permitted.
What a code gate must enforce
A code gate is a deterministic check that runs at merge time. It takes the set of files an agent proposes, runs them through a TypeScript schema validator, and either returns a PASS or a detailed FAIL response. The validator must be model‑independent: it does not rely on any AI model at runtime, only on the static type system and the project's declared contracts. The contract is expressed as a set of .d.ts files or a zod schema that the repository already uses. The gate’s job is to guarantee that every new piece of agent code conforms to those contracts before it becomes part of the codebase.
Worked example
Consider a repository that defines a User interface in src/types.ts:
export interface User {
id: string;
email: string;
role: 'admin' | 'member';
}
An agent proposes a new file src/generated/userService.ts that implements a helper:
export function createUser(data: any): User {
// naive implementation
return {
id: data.id,
email: data.email,
role: data.isAdmin ? 'admin' : 'member',
};
}
The merge‑time validator loads the project's User schema and type‑checks the new file. It discovers that the function accepts any, which bypasses the contract that callers must supply a User‑shaped payload. The validator emits:
ERROR: src/generated/userService.ts:2
Parameter 'data' is of type 'any' but should be 'Partial<User>'.
Refactor to accept a typed argument.
Because the validator returns a FAIL, the pull request is blocked. The developer (or the supervising system) can correct the signature to createUser(data: Partial<User>) and push again. The next validation passes, and the merge proceeds.
Embedding the validator in the merge workflow
The validator can be run as a GitHub Action, a GitLab CI job, or any CI system that supports a pre‑merge hook. The steps are:
- Checkout the PR head.
- Install the project's TypeScript compiler and any schema libraries (e.g.,
zod). - Run the custom validation script, which imports the project's schema definitions and type‑checks the new files.
- If the script exits with a non‑zero status, the CI job fails and the PR shows the error messages.
Because the validator is a pure TypeScript program, its output is repeatable: the same input files always produce the same pass/fail result. This property aligns with the broader principle of treating the agent write as a Build Intent gate, as described in the pillar post on governing code agents at the build‑intent gate. The merge validation is simply the final enforcement point before the repository mutation occurs.
The validator can be packaged as a small npm module and reused across projects. The SHPBL TypeScript client (@shpbl/sdk) demonstrates how a typed client can be built around a repeatable backend; the same pattern applies to a local validator that consumes the same schema contracts.
Aligning the gate with licensing and provenance
When an agent proposes code, the Build Intent gate first records the proposal, resolves licensing invariants, and returns a terminal state. The merge‑time validator sits downstream of that gate: it does not replace the licensing check, but it ensures that the code that passes the licensing gate also respects the technical contracts. If the validator fails, the proposal remains PROVISIONAL; the agent can iterate without consuming entitlement credits. This separation of concerns prevents the repository from being polluted by code that is legally sound but technically broken.
What to do on Monday
- Identify a single public TypeScript interface that your repository treats as a contract (e.g.,
User). - Add a small CI job that runs
tsc --noEmiton any new files introduced by a PR. - Configure the job to fail the PR when type errors appear, and surface the compiler output as feedback.
With those three steps in place, you have a functional merge‑time validator that blocks non‑conforming agent code. The next time an agent writes a file, the CI will either let the merge happen or return a clear error, keeping the main branch clean without sacrificing automation.
This post supports the longer argument in Governing Code Agents at the Build Intent Gate.
Keep reading
Next in the log
- Governing Code Agents at the Build Intent Gate
Why prompt guardrails fail on coding agents, and how to govern mutations by placing a deterministic build gate between intent and disk writes.
- Separating Agent Proposals from Repository Mutation Gates
Prevent agents from polluting git history by routing proposed diffs through an ephemeral validation gate before granting write access.
- Embedding Build Intent Checks into Pull‑Request Templates
Embedding a verified Build Intent token in PR templates creates a deterministic gate that stops unapproved agent writes before merge.
The Strategic Master Library · written and reviewed under the house's own epistemic rules: nothing claimed that we cannot show.