The `[ci skip]` Tag Is a Metric: Measuring How Often Your Team Bypasses Its Own Quality Gates

Every CI provider gives developers a documented way to skip the pipeline. GitHub Actions recognizes five commit-message tokens plus a trailer; GitLab recognizes two tokens plus a push option. That is not a bug. It is a release valve, and release valves are supposed to be measured. If your team cannot answer “how many times did we skip CI last month, and who did it,” you do not have a quality gate. You have a suggestion.

This is the argument: treat the skip token as a first-class metric, not as an etiquette problem. Publish the number. Attribute it. Decide, per repository, whether the number is acceptable. The rest of this article is the mechanics of doing that without guessing at provider behavior.

What the providers actually recognize

Start with the exact strings, because “everyone knows” is how you end up with a skip token that does nothing.

GitHub Actions documents that workflows triggered by on: push or on: pull_request will not run if the commit message contains any of the following strings:

  • [skip ci]
  • [ci skip]
  • [no ci]
  • [skip actions]
  • [actions skip]

GitHub also supports a trailer form: skip-checks:true or skip-checks: true, placed at the end of the commit message and preceded by two empty lines. If other trailers exist, skip-checks must be last. Git removes consecutive newlines by default, so preserving the exact message requires --cleanup=verbatim on the commit.

Two constraints matter for measurement. First, skip instructions apply only to push and pull_request events. A workflow triggered by pull_request_target is not skipped by these tokens. Second, skip instructions apply only to the workflow runs that the tagged commit would have triggered. A later commit without the token triggers normally.

GitLab documents a narrower set. For a push, adding [ci skip] or [skip ci] — any capitalization — to the commit message skips the pipeline. GitLab also supports the ci.skip Git push option (Git 2.10 or later), which does not skip merge request pipelines. For merge requests, GitLab documents adding [ci skip] or [skip ci] to the merge request title to skip basic merge request pipelines, merged results pipelines, and merge train pipelines.

The asymmetry is the point. If your organization runs both GitHub and GitLab, a single regex over commit messages will undercount on one side and overcount on the other. [no ci] is a GitHub token; GitLab does not list it. The ci.skip push option never appears in a commit message at all, so a commit-message scan cannot see it. Any bypass metric that only greps commit messages is measuring a subset of the behavior it claims to measure.

Why the skip is not the same as a missing run

GitHub’s documentation is explicit about the failure mode that makes this metric urgent: when a workflow is skipped due to path filtering, branch filtering, or a commit message, the checks associated with that workflow remain in a Pending state. A pull request that requires those checks to be successful is blocked from merging. The documented remedy is to push a new commit without the skip instruction.

That is a real safety property, and it is also a trap. It means the skip token does not silently disable your gate — it converts the gate into a stuck state that a human has to resolve. The resolution is usually “push an empty commit,” which is itself a bypass of the intent, even if it satisfies the letter of branch protection. If you are counting skips, count the follow-up empty commits too. They are the same event wearing a different hat.

GitLab behaves differently and this matters for the metric. GitLab documents that a skipped pipeline still creates an empty pipeline with no jobs or stages. That pipeline appears in the UI and is returned in API responses, with status Skipped in the UI and skipped in the API. This is the single most useful fact in this article: on GitLab, the bypass is a queryable object, not an absence.

On GitHub, the skipped run is an absence. There is no run to query. The evidence lives in the commit message and in the pending check state, not in a run record. That difference should shape how you build the metric, and it should shape which provider you pick if bypass auditing is a hard requirement.

Building the metric

There are two honest ways to count bypasses, and they answer different questions.

Method 1: commit-message scan. Walk the commits on the default branch for the period, match the documented tokens per provider, and count. This is cheap, works identically on both providers, and captures the trailer form on GitHub. It misses the GitLab ci.skip push option entirely, and it will produce false positives on commits that mention the token in prose (for example, a commit that updates the CI documentation). You can reduce false positives by anchoring the match to the commit subject or to the trailer block, but you cannot eliminate them. Report the method alongside the number.

Method 2: pipeline-state query. On GitLab, query the pipelines API for status skipped over the period. This is the accurate count for GitLab, because the skipped pipeline is a real record. On GitHub, there is no equivalent — you cannot query for runs that did not happen. The closest approximation is to query for check suites stuck in a pending state, which is noisy and conflates skips with path-filtered workflows.

The practical recommendation: run Method 1 as the cross-provider baseline, and Method 2 as the GitLab-specific ground truth. When the two disagree on GitLab, trust Method 2 and treat the gap as the size of your ci.skip blind spot. That gap is itself a useful number — it tells you how much of your bypass behavior is invisible to a commit-message audit.

Attribute every bypass to an actor and a repository. A team-level skip rate with no attribution is a number nobody can act on. The commit author is the actor for commit-message skips; the push-option actor is whoever ran the push, which may differ from the commit author. If you cannot resolve the actor, say so in the report rather than guessing.

What the number is for

A bypass rate is not a performance metric for individuals. It is a signal about the gate itself. Three readings are worth distinguishing:

High skip rate on documentation-only repositories. This is usually correct behavior. The gate is expensive and the change is inert. The right response is a path filter, not a policy. GitHub documents path filtering as a first-class trigger control; use it and the skip token becomes unnecessary for this case.

High skip rate on the main application repository. This is the case that should trigger a review. Either the pipeline is too slow to run on every commit, or the gate is failing for reasons developers do not trust. Both are fixable, and neither is fixed by removing the skip token. Removing the token without fixing the cause produces empty commits, which are worse because they are invisible.

Skip rate concentrated in a small number of actors. This is a conversation, not a policy change. The metric’s job is to make the conversation possible with a number instead of an anecdote.

Set the threshold per repository, in writing, with a named owner. “We accept up to N skips per month on this repo; above that, the platform team reviews the cause” is a policy. “Please use skip sparingly” is not.

Can branch protection stop it?

Partially, and the limits are documented.

GitHub’s protected-branches documentation states that required status checks must have a successful, skipped, or neutral status before collaborators can merge. Note the word skipped in that list. A skipped check can satisfy a required status check. This is the loophole that makes the skip token a genuine bypass rather than a self-blocking mistake, and it is why the pending-state behavior described earlier is not a complete defense.

GitHub also documents that by default, branch protection restrictions do not apply to people with admin permissions or custom roles with the “bypass branch protections” permission. The Do not allow bypassing the above settings option applies the restrictions to admins and those roles as well. If your bypass metric shows skips from admins, this setting is the first thing to check.

GitLab’s merge request approvals documentation is more explicit about enforcement. Required approvals are a Premium and Ultimate feature; on GitLab Free, approvals are optional and do not prevent merging. If you are on Free and relying on approvals as a compensating control for skips, you do not have that control. GitLab also documents that pipeline execution policies and scan execution policies can restrict or disable the [skip ci] directive, which is the closest thing to a provider-level kill switch on the skip token.

The honest summary: on both providers, the skip token is a documented, supported feature, and branch protection does not reliably neutralize it. If you want the gate to be non-bypassable, you need a control outside the CI trigger — a deployment gate, a required reviewer, or a policy that restricts the directive. Those are the compensating controls the providers document, and they are the ones worth evaluating.

A minimal monthly procedure

  1. Define the period and the repositories in scope. Write both down.
  2. For each repository, extract commits on the default branch for the period.
  3. Match the provider-specific token set. GitHub: the five bracketed strings plus the skip-checks trailer. GitLab: [ci skip] and [skip ci], case-insensitive.
  4. On GitLab, separately query the pipelines API for status skipped and reconcile against the commit-message count. Report the gap.
  5. Attribute each match to an actor and a repository. Flag unresolvable actors rather than dropping them.
  6. Compare against the written threshold for that repository. Above threshold: open a review, not an incident.
  7. Publish the number where the team can see it. A metric that lives in a private dashboard is a metric nobody argues with, which means it changes nothing.

Two caveats on the procedure. First, the GitLab ci.skip push option is invisible to step 3 and only visible in step 4, and only on GitLab. Second, the GitHub trailer form requires the trailer to be the last one and preceded by two empty lines; a commit that violates that layout will not be skipped, so a match on the trailer string is not proof that a skip occurred. Verify against the run history before reporting a trailer match as a bypass.

What to do with the number

Publish it, attribute it, and set a threshold per repository. Do not remove the skip token as a first move — the token is not the problem, it is the symptom. The problem is either a pipeline that costs more than the change is worth, or a gate the team has stopped trusting. The skip rate tells you which one you have. That is the whole value of the metric: it converts an invisible habit into a number you can argue about with evidence instead of opinion.

FAQ

Does [no ci] work on GitLab? GitLab’s documentation lists only [ci skip] and [skip ci] for commit messages. Do not assume the GitHub token set transfers.

Does [skip ci] stop a pull_request_target workflow on GitHub? No. GitHub documents that skip instructions apply only to push and pull_request events.

Can I see a skipped run in the GitHub Actions run history? No. The run does not exist. The evidence is the commit message and the pending check state.

Can I see a skipped pipeline in GitLab? Yes. GitLab creates an empty pipeline with status Skipped, visible in the UI and returned by the API.

Does a skipped check satisfy GitHub branch protection? Yes, if the check is required. GitHub’s documentation lists skipped as an acceptable status alongside successful and neutral.

Can GitLab block the skip directive? GitLab documents that pipeline execution policies and scan execution policies can restrict or disable the [skip ci] directive. Availability depends on your tier.

Is a high skip rate always bad? No. On documentation-only repositories it is often correct. The metric is a prompt for review, not a verdict.