---
title: "Waive a run gate"
method: POST
path: "/projects/{projectId}/eval-runs/{runId}/gate-waivers"
tags: ["Eval runs"]
---

# Waive a run gate

`POST /projects/{projectId}/eval-runs/{runId}/gate-waivers`

Override a FAILING run's release gate until an expiry you name. This does NOT make the run pass: the run keeps its `result`, the waiver is a separate audited record, and every surface that honors it — the GitHub Check Run and the CLI's `eval gate` — reports the gate as waived, by whom, why, and until when.

Requires the manage tier; whoever launched the run gets no exception for having launched it. `reason` is stored UNREDACTED and readable by anyone who can see the suite, for as long as the suite exists.

`400` covers five distinct refusals, each with a message written for the caller: the suite belongs to no organization, the reason is blank, the reason exceeds 500 characters, the expiry is not in the future, or the expiry is more than 30 days out. `403` means the caller can see the suite but lacks the manage tier — deliberately not collapsed to `404`, which would send a legitimate member hunting for a run sitting in front of them.

## Path parameters

- `projectId` string, required
- `runId` string, required

## Request body

- object
  - `reason` string, required — Why the gate is being overridden. Non-blank, at most 500 characters, and THE RECORD of the decision. Stored unredacted for the life of the suite.
  - `expiresAt` integer, required — When the waiver lapses, as epoch milliseconds. Must be in the future and at most 30 days out.

## Response `201`

The waiver was granted.

- GateWaiverWriteResult — The result of granting or revoking a waiver. `conflict` and `already_revoked` are IDEMPOTENT no-op successes, not failures.
  - `status` 'created' | 'conflict' | 'revoked' | 'already_revoked', required — `conflict` — a waiver was already in force, and `waiver` is that EXISTING one rather than a second row. `already_revoked` — this waiver had already been revoked, and `waiver` reports the original revocation rather than restamping it, so the record of who actually ended it survives a second call.
  - `republishedChecks` integer, required — GitHub Check Runs brought back in line by this write. A published check is a persisted verdict, not a live read, so `0` on a repository with checks connected means the status that gates the merge did not move.
  - `waiver` GateWaiver, required — An audited, time-boxed override of an eval run's release gate. A waiver never changes the run's own `result` — the run keeps its verdict and every surface that honors the waiver says so out loud, which is what makes "no silent waiver" checkable rather than promised.
    - `id` string, required
    - `suiteId` string, required
    - `runId` string, nullable, required — The run this waiver covers. Suite-wide waivers are not honored.
    - `reason` string, required — Why the gate was overridden, as the granter wrote it. Stored UNREDACTED and readable by anyone who can see the suite, for as long as the suite exists — never put secrets, tokens, or customer data in it.
    - `expiresAt` integer, required — Epoch ms. Always in the future when granted, and capped at 30 days out — there is no permanent waiver.
    - `createdAt` integer, required
    - `createdBy` string, required
    - `createdByEmail` string, nullable, required — `null`, never absent, when it cannot be resolved — a deleted user must not make a waiver look authorless.
    - `revokedAt` integer, nullable, required
    - `revokedBy` string, nullable, required
    - `active` boolean, required — Whether it is in force right now — neither revoked nor expired. Computed at read time; a client that must not honor a lapsed waiver should re-derive it from `expiresAt` rather than trust it.
    - `policySnapshot` object, nullable, required — WHAT was overridden, captured at waive time so a later edit to the suite cannot rewrite the record. `null` for a run decided by the v2 verdict policy, whose identity is recorded on the audit event instead — this shape cannot hold it, and filling it in would be a false record rather than an incomplete one.
      - `minimumPassRate` number, required

## Other responses

- `400` — The waiver was refused. The message names which of the five conditions failed and what would fix it.
- `401` — Missing, invalid, revoked, or orphaned key (`UNAUTHORIZED`) — or the **target MCP server** needs an OAuth grant (`OAUTH_REQUIRED`), which is a property of the server, not your key.
- `403` — Key is valid but not allowed to do this.
- `404` — Unknown project, server, or resource.
- `409` — A waiver is already in force over this run. The body carries that EXISTING waiver; no second one was granted, because two active waivers would make "which reason is on the check" a race.
- `429` — Per-key rate limit exceeded (60 requests/minute sustained, bursts up to 10). Honor `Retry-After` and back off with jitter.
- `500` — Something failed on MCPJam's side.

## Changes

- **2026-08-25** `172afb703db1` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/mcpjam/apis/mcpjam-api/changes/projects/:projectId/eval-runs/:runId/gate-waivers/post.md)

---

[API](https://skmtc.dev/mcpjam/apis/mcpjam-api.md) · [All operations](https://skmtc.dev/mcpjam/apis/mcpjam-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/mcpjam/mcpjam-api/revisions/3d62c6919d8d/schema)
