Skip to main content
skill CX Codex

pre-push-gate — Claude Code skill: what it does, how to install

Runs the repository quality gate before pushes and troubleshoots failing stages like linters, tests, DCO, and skill manifests.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add modelcontextprotocol/servers --skill pre-push-gate -a claude-code
Open on GitHub

What pre-push-gate does

This skill guides the agent through running npm run local:gate in modelcontextprotocol/servers. It formats TypeScript files with npm run format or Python servers with uv run --frozen ruff format ., runs the gate, and reads the numeric exit code directly instead of grepping console text.

When a stage fails, the skill provides direct resolution steps. It covers running npm install for verify:install-fresh, fixing unsigned commits with git rebase --signoff for DCO, pinning GitHub Actions to exact commit SHAs, and fixing formatting coverage. If one check fails, the gate halts immediately, requiring a full rerun after the fix.

Running the gate offline will fail because verify:skills:cli downloads the pinned Claude Code CLI via npx whenever the local version differs.

What it helps with

  • run npm run local:gate and verify the exit code before pushing
  • fix DCO sign-off issues across branch commits
  • sync stale lockfiles and node_modules during verify:install-fresh
  • pin GitHub Action commits by SHA in credentialed workflow jobs

Example prompts for the agent

  • Run the pre-push gate and check if it exits with zero.
  • Fix the verify:dco failure on my current branch.
  • The local gate failed on verify:skills, resolve the manifest issue.

How to install pre-push-gate

1Skills CLI (Vercel)

npx skills add modelcontextprotocol/servers --skill pre-push-gate -a claude-code

2Manually: put the files in place

~/.claude/skills/pre-push-gate/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/pre-push-gate/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about pre-push-gate

How do I install pre-push-gate in Claude Code?

Run npx skills add modelcontextprotocol/servers --skill pre-push-gate -a claude-code. You can also place the skill file manually at ~/.claude/skills/pre-push-gate/SKILL.md.

What is the pre-push-gate skill?

It provides automated instructions for executing the repo quality gate and diagnosing broken stages before pushing code.

Can the gate run without an internet connection?

No, because verify:skills:cli invokes npx to fetch the pinned Claude Code CLI when versions differ, causing offline runs to fail.

Source text

SKILL.md · Apache-2.0
---
name: pre-push-gate
description: "Run this repo's pre-push gate and fix it when it goes red. Use when npm run local:gate fails or a stage of it does (verify:install-fresh, a root guard, lint, typecheck, ruff, pyright, pytest, verify:skills:cli, the boot smoke); when the gate says it is waiting on the gate lease; or when checking whether a gate run really passed."
disable-model-invocation: false
---

# The pre-push gate

**The rule** (stated in [`AGENTS.md`](../../../AGENTS.md#before-pushing)): run
**`npm run local:gate`** before every push, and push only when it exits 0.
`npm run validate` is the inner-loop check and is **not** a substitute.

What each stage covers, and how the gate relates to CI, is
[`docs/quality-gate.md`](../../../docs/quality-gate.md). The stage list is
written out there and nowhere else, on purpose. This skill is how to run the
gate and what to do when it goes red.

## Run it

```sh
cd <repo root>
npm run format                       # TypeScript. It rewrites files; the gate only checks.
npm run local:gate; echo "EXIT=$?"
```

In a Python server you changed, format with `uv run --frozen ruff format .`
from that server's directory.

**Verify by exit code, not by grepping the output.** A Prettier failure is a
few `[warn]` lines that match no obvious failure pattern, and `validate:py`
prints `PASS` for two servers above the `FAIL` for the third.

⚠️ If you run it as a background task, the harness's "exit code 0"
notification describes the _wrapper_ (the trailing `echo`), not the gate. Read
the `EXIT=` line.

**Background it and wait for the notification.** Do not spend turns tailing
the log; see `AGENTS.md` **Waiting on long-running work**.

The gate stops at the first failing stage, so the last stage header in the
output is the one that failed. Fix it, then **re-run the whole gate**, not just
that stage: the stages after it have not run yet.

## Diagnosing a failing stage

Each heading is a stage or guard name as it appears in the output.

### `verify:install-fresh`

An installed package's version disagrees with `package-lock.json`
(`node_modules` is older than the tree you pulled), or a `package.json`
declares a dependency the lockfile does not record. **Run `npm install` at the
repo root**, commit the lockfile if it changed, and re-run. It is the first stage because a stale install otherwise
passes every static check and fails later as a test reporting the _old_
dependency's behavior. Do not "fix" that test.

### `verify:dco`

A commit in `origin/v2/main..HEAD` has no `Signed-off-by:` trailer, or one
whose name and email match neither its author nor its committer. The output
names each commit and the repair: `git rebase --signoff <merge-base>`, then
`git push --force-with-lease` if it was already pushed. Commit with
`git commit -s` from then on. The rule, and when a rewrite is safe, is
`pr-flow` step 3.

"cannot resolve origin/v2/main" means the ref is missing from this clone:
`git fetch origin v2/main`. On a stacked branch the gate also checks the lower
branch's commits; `npm run verify:dco -- --base <lower branch>` checks only
yours.

### `format:check` / `format:check:root`

Run `npm run format` at the **root**. It covers `scripts/`, the root configs
and every workspace; a single workspace's `format` does not.

### `verify:format-coverage`

A tracked source file is matched by no `format:check` glob, so nothing checks
its formatting. Put it where a glob reaches it, or extend the glob. Do not add
it to `.prettierignore` to make the guard pass.

### `verify:skills` / `verify:skills:cli`

A `.claude/skills` manifest does not parse, declares no
`disable-model-invocation`, a model-invoked skill lacks its eval cases, or the
listing is over budget. The message names the skill. The usual cause is an
unquoted `#` or `:` in a `description`.

`verify:skills:cli` is the authoritative validator. It fetches the pinned
Claude Code CLI with `npx` when the installed one is a different version, so it
**fails offline**. (`validate:py`'s `sync` step is the other stage that can:
`uv sync --locked` downloads when a server's environment is missing or stale.
`coverage:py` opens with the same sync, a no-op once `validate:py` has run.)

### `verify:typecheck-coverage`

A tracked `.ts` file lands in no `tsc` program. Usually a new test or config
file outside the server's `tsconfig.test.json` `include`. Add it there.

### `verify:dep-lockstep`

Two workspaces declare different ranges of a shared toolchain package
(`typescript`, `vitest`, `@vitest/coverage-v8`, `prettier`, `@types/node`).
Bump it in **every** workspace that declares it, then `npm install`.

### `verify:no-test-retries`

A test declares a retry: Vitest's `retry:` option or `--retry` flag, a pytest
rerun plugin, or a `flaky` marker. Remove it and fix why the test fails. A race
is fixed with fake timers or an awaited condition; a port collision with a port
the OS hands out.

The guard matches spellings, not semantics, so an unrelated `retry:` property
in server source trips it too. Rename the property, or restructure so the key
is not written as `retry:` at the start of an expression.

### `verify:action-pins`

A job that holds a credential, one whose artifact such a job downloads, or one
whose outputs such a job reads, uses an action by a tag (`actions/checkout@v6`) or by a SHA with no exact-version
comment. The finding names the workflow, the job and the `uses:` value. Pin it
as `owner/repo@<40-hex sha> # vX.Y.Z`, resolving both from one lookup:

```sh
ACTION=actions/checkout TAG=v6.1.0
echo "uses: $ACTION@$(gh api "repos/$ACTION/commits/$TAG" --jq .sha) # $TAG"
```

If the job should not have become credentialed, that is the real fix: the
guard counts `id-token: write` or `packages: write` (the job's own, or the
workflow's when the job declares none); any secret other than `GITHUB_TOKEN`;
an `upload-artifact` in a job that a credentialed downloader has anywhere in
its `needs` chain; a reusable-workflow call in that position; and any job
whose outputs a credentialed job reads (`needs.<job>.outputs`), which is why
`release.yml`'s `detect-packages` is pinned although it holds no credential
itself. A local `./…` action or workflow in such a job is reported as well. A YAML
alias in such a job is reported too; spell the ref out. The rule is in
`AGENTS.md` **Credentialed workflow jobs**.

### `test:scripts`

A guard's own unit test failed. Two of these are about the gate itself and
mean a change broke one of its invariants
(`scripts/lib/workflow-gate.test.mjs`):

- **"GitHub CI must not run a local-only script"**: a workflow invokes a
  `local:*` script. Call the underlying checks as their own steps instead.
- **"every CI job needs a timeout"**: a job was added without
  `timeout-minutes`.
- **"local:gate must run …"**: a stage was dropped from `local:gate:stages`,
  or `local:gate` is no longer exactly the lease wrapper.

Do not edit the guard to make these pass; the split they enforce is the design.

### `lint`

**There is no warning tier.** Every `lint` script runs `--max-warnings 0`, so
a warning fails exactly as an error does. Fix the finding. If a rule genuinely
must be waived on one line, use its inline disable comment **with a one-line
justification**; never widen an ignore list or turn the rule off.

### `typecheck` / `build`

`typecheck` runs `tsc -p tsconfig.test.json` (source and tests); `build` runs
the server's own `tsc`. An error in a test file only shows in `typecheck`.

### `test` (a TypeScript workspace)

Re-run just that suite to iterate: `npm test -w src/<server>`. Read the failure
before changing a timeout: an assertion that races is a bug in the test.

### `coverage`

A TypeScript file is below 90% on lines, statements, functions or branches.
Vitest names each file and dimension:

```
ERROR: Coverage for branches (71.42%) does not meet global threshold (90%) for roots-utils.ts
```

("global threshold" is Vitest's wording; with `perFile: true` it is checked
per file.) The root `npm run coverage` carries on into the remaining
workspaces after a failure, so **the failing report is not the last thing
printed**: find the `npm ERR!` block, which names the workspace
(`in workspace: @modelcontextprotocol/server-<name>`), and read the `ERROR:`
lines just above it.

1. Re-run that server alone: `npm run coverage -w src/<server>`.
2. Read its per-file table. The `Uncovered Line #s` column is where the
   missing tests go (a long list is truncated to `...`, and a long file name
   to `...-name.ts`; open `coverage/index.html` in the workspace, or narrow
   the run, for the full list). A branch shortfall with no uncovered lines is
   the untaken side of a condition, `??`, `?.` or default parameter.
3. **Write the tests.** Drive what a client sees through a `Client` over the
   in-memory transport; call a helper directly only for its logic (`testing`
   skill).
4. Only for code that genuinely cannot run, annotate it at the source with
   its reason, `/* v8 ignore next -- <reason> */` (or `start`/`stop` around a
   block). A branch that is merely awkward to reach is not unreachable.

**Never lower a threshold, drop `perFile`, or widen the coverage `exclude`**
to get green; the gate is the rule in `AGENTS.md` **Always test new or
modified code**. A failing _test_ in this stage is an ordinary test failure:
fix it as under `test`, above.

### `validate:py`

The summary at the end names the failing server, and the step that failed is
the last `[validate:py] <server>: <step>` line above it. Re-run one server with
`npm run validate:py -- <server>`.

| Step | Fix |
| --- | --- |
| `sync` | `uv.lock` no longer matches `pyproject.toml`. Run `uv lock` in that server and commit the lockfile |
| `ruff check` | Fix the finding; `uv run --frozen ruff check --fix .` handles the mechanical ones |
| `ruff format --check` | `uv run --frozen ruff format .` |
| `pyright` | A type error. Add the missing hint rather than an ignore |
| `pytest` | `uv run --frozen pytest -k <name>` in that server to iterate |
| `build` | A packaging error in `pyproject.toml` |

`uv: command not found` means `uv` is not installed. Never substitute `pip`.

### `coverage:py`

The Python per-file coverage gate: every file each server measures must reach
**90% on lines and 90% on branches**. The summary names the server, the step,
and for a shortfall each file and dimension below 90:

```
  FAIL  time — per-file check (2 below 90%)
          src/time/src/mcp_server_time/server.py: lines 84.2% (75/89) is below 90%
          src/time/src/mcp_server_time/server.py: branches 78.5% (11/14) is below 90%
```

Re-run one server with `npm run coverage:py -- <server>`. The per-file table
above the summary gives both figures for every file, and coverage.py's own
report above that (`term-missing`) lists the uncovered lines and partial
branches (`120->124` is the jump from line 120 to 124 never taken).

| Step | Fix |
| --- | --- |
| `sync` | As for `validate:py`: run `uv lock` in that server and commit the lockfile |
| `pytest --cov` | A test failed, so no verdict was reached; fix it as for `validate:py`'s `pytest`. `unrecognized arguments: --cov` means `pytest-cov` is missing from the server's dev dependencies |
| `per-file check`, "below 90%" | Write tests for the lines and branches the report lists. Drive the server in-process through a `ClientSession` where you can (`/testing`) |
| `per-file check`, "was not written" or "measured no files" | The run produced no usable `coverage.json`: check the server's `[tool.coverage.run]` (`branch = true`, `source` naming its package) |
| `per-file check`, "not measured with branch coverage" or "no covered_branches/num_branches" | `branch = true` is missing from `[tool.coverage.run]`. The check refuses such a report rather than counting every file's zero branches as 100% |

**Never lower the gate**, and never drop a file from `source` to clear it.
Code that genuinely cannot run (a guard the types already rule out, a branch
the SDK's input validation makes unreachable) is marked at the source with
`# pragma: no cover  # <reason>`; every pragma carries its reason
(`AGENTS.md` **Always test new or modified code**). A pragma on a line that is
merely awkward to test is not justified; write the test.

### `smoke`

`smoke — FAIL  <server> over <transport>` followed by the reason and the tail
of the server's stderr. Re-run a subset with
`node scripts/smoke-servers.mjs <server> [<server> …]`.

- **"dist/index.js does not exist — build first"**: you ran the smoke on its
  own before building. `npm run build -w src/<server>`. Inside the gate this
  cannot happen, since `validate` builds first.
- **".venv does not exist — sync first"**: the same for a Python server. The
  smoke never creates an environment; `npm run validate:py -- <server>` does.
- **The server exited before answering**: the stderr tail shows why. This is
  what the smoke is for: the unit tests import the source, so a wrong `bin`
  path, a file missing from `dist/`, or an import that only resolves under the
  test runner passes them and fails here.
- **"tools/list does not include …"**: a tool was renamed or removed. A
  published tool is never renamed (`AGENTS.md` **MCP protocol**); if the change
  is intended, update that server's entry in `SERVERS`.
- **A new server fails `test:scripts`** with "every server directory in the
  repo has a smoke entry": add it to `SERVERS` in `scripts/smoke-servers.mjs`.

### `interface-diff`

The last stage diffs the `everything` server's MCP interface (capabilities,
tools, prompts, resources, resource templates) in this checkout's build
against the same server built from the merge-base with `origin/v2/main`, and
prints the report. **A changed interface does not fail it**: the report is
what the PR's sticky comment will show, so read it, and if a change is not
one you meant, that is the finding. It fails only when the diff cannot run,
with `❌ The interface diff could not run` and the reason:

- **"origin/v2/main is not available"**: `git fetch origin v2/main`.
- **"src/everything/dist/index.js does not exist"**: you ran it on its own
  before building: `npm run build -w src/everything`.
- **An `npm ci` or `npm run build` failure**: the _base_ did not install or
  build. Offline with a cold npm cache is the usual cause; otherwise name a
  base that builds with `npm run interface-diff -- --base <ref>`.
- **"Base server probe failed" / "Target probe failed"**: a server did not
  answer the probe. A target failure is this checkout's server and is the
  real defect; the smoke stage before it usually fails first.

Re-run it on its own with `npm run interface-diff` (about ten seconds). What
it compares against in CI, and why it is a script rather than an action, is
in [`docs/quality-gate.md`](../../../docs/quality-gate.md) and the header of
`scripts/interface-diff.mjs`.

## Waiting on the lease

A gate that starts with

```
gate-lease: pid 12345 in /path/to/other-worktree, running for 40s holds the gate lease; waiting for its turn so the gates do not contend. SERVERS_SKIP_GATE_LEASE=1 runs anyway.
```

is queued behind another worktree's gate. That is normal: it starts once the
holder (and any gate that arrived before it) finishes. The line names the
holder's pid and worktree, so you can tell whether to wait or to stop that
gate.

- A holder that was **killed** stops refreshing its lock and is taken over
  after 30 seconds. Nothing needs cleaning up.
- A waiter that prints `gave up after 45m` names a lock path. Check whether
  the holder it names is alive and hung (stop it), or dead with a lock
  directory that could not be removed (remove that directory).
- Do **not** set `SERVERS_SKIP_GATE_LEASE=1` to get a result sooner. Two
  overlapping gates each take longer than one queued behind the other.
- Do **not** write your own "wait until no gate is running" loop. Two of them
  deadlock on each other, and a `pgrep` for the gate's command line matches the
  loop itself.

The details of the lease are in
[`docs/quality-gate.md`](../../../docs/quality-gate.md#the-gate-lease).

## Not part of the gate

`npm run skills:eval` (real model calls) and `npm run pack:verify` (installs
each package's publish artifact and boots it; needs the network, and belongs to
the release flow) are separate commands. Run the eval suite when you add a
skill or change a description.

© modelcontextprotocol · Apache-2.0 · modelcontextprotocol/servers

Similar skills

All skills for Git and GitHub →

From the same set11

testing The skill covers where tests live for each MCP server, which runner picks them up, and how to clear the per-file coverage gate. Useful when a test is skipped or passes against a stale build. CX Codex +1 ★ 91,595repo stars security-advisory Guides private vulnerability triage for GitHub Security Advisories: drafts board cards, audits code ownership against SDKs, and tracks private fork fixes. CX Codex +1 ★ 91,595repo stars release Walks one milestone release from the release issue through audit, version bumps and the merge PR to the GitHub Release a maintainer publishes. CX Codex +1 ★ 91,595repo stars project-structure This skill maps the layout of the modelcontextprotocol/servers repo and tells you which file or directory a new piece of code belongs in. CX Codex +1 ★ 91,595repo stars pr-flow Walks a task from issue to pull request and closes it after merge: branch, DCO signoff, pre-push gate, Copilot review. CX Codex +1 ★ 91,595repo stars local-dev A skill for local MCP server work: install dependencies, build the TypeScript workspaces, and start a server over stdio, SSE or Streamable HTTP. CX Codex +1 ★ 91,595repo stars issue-triage Triages incoming GitHub issues and external pull requests, scores priority, and sweeps valid tasks onto the Servers V2 project board. CX Codex +1 ★ 91,595repo stars issue-create Walks an issue from duplicate check to a board card with status and priority. Useful when work turns up mid-task and needs its own issue. CX Codex +1 ★ 91,595repo stars

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor