Gates for coding agents: where to stop an agent before push, PR and deploy
Checks in the hook path have microseconds of budget. Checks at push, pull request and deploy can wait. A stage map of deterministic gates for coding agents, with the sequence policies that make a cheap check smarter than it looks.
A gate for a coding agent is a deterministic check that one action cannot pass until a condition holds. Put gates where the action happens, and pick the place by what the check needs: the hook path for decisions that must not wait on a network round trip, the push, pull request and deploy boundaries for checks that can take seconds or minutes.
Two speeds, four places
| Where the gate runs | Time it may take | What it can check | What it cannot do |
|---|---|---|---|
| Hook path, per tool call | Microseconds to low milliseconds | Patterns, policy-as-code, session state | Call a model or the network |
| Push | Under a second locally, seconds on the server | Branch, commit, review state | See anything that happened before the push |
| Pull request | Minutes | Builds, tests, SAST, human review | Stop a direct push on a branch that does not require a PR |
| Deploy | Minutes | Release policy, environment approval | Fix a bad commit before it merges |
The hook path
A hook fires on every tool call in an agentic loop, and one session can make a few hundred of them. That budget rules out anything that waits: no model call, no cloud policy service, no remote allowlist. What fits is deterministic work in the process: matching on the tool name and arguments, policy-as-code compiled from Lyra or OPA, and a small run-state automaton that remembers what happened earlier in the session. All of it works offline, which matters on a plane and matters more on a build machine with no route out.
Three vendor examples, with their real limits.
Claude Code reads PreToolUse hooks from .claude/settings.json, ~/.claude/settings.json, or managed settings. A handler returns permissionDecision: "deny" with a reason, or exits 0 and says nothing. The optional if field narrows a handler to something like Bash(git *), and the docs say plainly that if is best-effort and that a hard allow or deny belongs in the permission system rather than a hook. allowManagedHooksOnly in managed settings restricts which hooks may run (hooks reference).
Cursor reads .cursor/hooks.json at project level, ~/.cursor/hooks.json at user level, and enterprise hooks from /Library/Application Support/Cursor/hooks.json on macOS, /etc/cursor/hooks.json on Linux and C:\ProgramData\Cursor\hooks.json on Windows. beforeShellExecution and beforeMCPExecution can return permission: "deny", and exit code 2 blocks. A crash, a timeout or any other non-zero exit fails open unless the hook sets failClosed: true (hooks reference).
Codex pauses according to approval_policy in ~/.codex/config.toml and limits filesystem and network reach with sandbox_mode, which OpenAI documents as OS-level enforcement, with no network by default and writes confined to the workspace. Administrators can constrain both from requirements.toml, so a managed machine cannot be set to approval_policy = "never" or danger-full-access (config basics, managed configuration).
Read those together and the shape is clear. Claude Code and Cursor give you a hook whose failure behaviour you have to choose. Codex gives you an OS-enforced sandbox plus an approval policy, with a managed file that can restrict both. Each is configured per vendor, so three agents mean three configurations, and none of them gives you one policy across three agents out of the box. The decisions land in that vendor's own record unless you export them.
Sequence policies are why a cheap check is not a dumb one
A stateless check has to be conservative, because it knows nothing about the session. A run-state automaton makes the same cheap check much smarter. Keep a handful of flags per session: read a secret, fetched from the web, review passed, wrote outside the task's files, previous deny. Then a rule is a lookup on the event and the state, evaluated in memory, in constant time. No model and no network involved.
| Rule | State it reads | Decision |
|---|---|---|
After reading a file matching **/.env*, do not allow network egress | read_secret | Deny egress for the rest of the session |
After a web fetch, do not allow git push | web_fetch and no review_passed | Deny the push |
Before github.pr.create, ask whether the diff is in scope | None | Hold for approval |
| After a policy deny, do not allow the same tool call with modified arguments | denied | Deny |
The last row is the one people forget. An agent that gets denied tends to retry with slightly different arguments, and a check that only looks at the current call will let the third attempt through.
Alyria publishes five worked examples in this shape, each with what happened, what was decided and why, on semantic policy examples. Lyra compiles the same rules into the local policy service and evaluates them on the endpoint (Lyra policy as code).
Push
git push is the first action that publishes the work to a shared remote. A hook at .git/hooks/pre-push receives the refs on stdin and can exit non-zero to abort, and git push --no-verify skips it (githooks). Any push gate that has to hold therefore lives on the server, as a ruleset that requires a pull request and required status checks, or as a policy on the receiving side.
Gates written as sentences:
- After a dependency install from an unpinned source, do not allow a push to
main. - After any write under
.github/workflows/, do not allow a push until someone with workflow permissions has reviewed it. - Before a force push to a protected branch, deny. This is not an approval case, because there is no good version of the action.
Pull request
The pull request is where the expensive checks belong, because only the merge is waiting. Required status checks cover the build, the unit tests, SAST, secret scanning and license review. Required review covers the second pair of eyes, which for an agent means someone other than the person who started the session.
Two rules that are specific to agents:
- After the agent opens a pull request, do not allow the agent to approve it.
- After review approved a revision, do not allow further commits to that branch without a fresh review.
The second one is the agent-era version of reviewing the thing that ships, and it is the rule that survives an agent that is helpful and keeps fixing things after the review.
Deploy
The deploy boundary is the last gate and the least ambiguous one. Environments with required reviewers, deployment protection rules and a wait timer cover the release. OIDC keeps the workflow from holding a stored cloud key (managing environments).
- Before a deploy to production, require an approval from the service owner.
- Before a deploy, do not allow a revision that did not pass the pull request gate.
- After a deploy, do not allow the agent to change the feature flag that turns the change on without an approval.
The third rule matters when the agent can reach the flag API, because a deploy is not the only way a change reaches customers.
What a gate is not
A gate is not a line in a prompt. "Do not push to main" in an AGENTS.md is advice the model may follow. A gate is a decision taken outside the model, by something that will not be talked out of it. A gate with no record is a future argument, so log the rule id, the decision, the mode and the revision it applied to.
The order to build them in
Two or three rules with no false positives first: secrets, force push, production deploy. Run them in monitor mode for a week and count what they would have stopped. Promote one category at a time, and keep the rule text in the repository, so a change to policy is a reviewable diff like any other.
That is the gate layer of governing agents across the SDLC. The other two parts cover the evidence to keep and how approvals fit without causing fatigue. The vendor behaviour described here comes from the Claude Code hooks reference, the Cursor hooks reference and the Codex configuration docs, read in November 2026, and the fail-open defaults are worth re-reading against the versions you run.