One GitHub Action that runs the checks an AI-era repo wants and reports them as a single pass/fail gate with a combined summary. It bundles AI-slop detection, a GitHub Actions security audit, an agent-skill scanner, and a bundled secrets grep, so a workflow adds one step instead of wiring four.
You still get four separate reports; they just show up under one job instead of four, and one bad finding anywhere fails the whole gate.
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: munzzyy/ci-safety-gate@v0.1.0That's it. Every check defaults to on. A full example, including a hardened checkout step,
is at examples/workflow.yml.
- noslop (
noslop-linton PyPI) — flags AI-written-code and AI-slop tells. Runs code mode over your source and prose mode over your docs. - zizmor — audits
.github/workflowsand anyaction.ymlfor GitHub Actions vulnerabilities: template injection, unpinned third-party actions, missingpersist-credentials: false, and the like. - skillxray — scans
SKILL.mdfiles, Claude Code plugins, and MCP bundles for prompt injection, hidden Unicode, dangerous commands, and leaked secrets. If your repo has none of that, this step detects it and skips cleanly instead of failing on nothing to scan. - secrets (bundled,
scan_secrets.py, no dependency) — greps the working tree for AWS access key IDs, GitHub and GitLab tokens, OpenAI/Anthropic/Stripe keys, and PEM private key blocks. Matched values are redacted before they ever hit the log.
Every check is its own input, "true" by default:
- uses: munzzyy/ci-safety-gate@v0.1.0
with:
skillxray: "false" # skip if you know you have no skill/plugin content
zizmor-min-severity: "high"
noslop-code-globs: "*.py *.ts"
secrets-exclude: "tests/fixtures/*"Full input list is in action.yml: paths and globs per check, a fail
threshold for skillxray and zizmor, a score threshold for noslop, and a size cap for the
secrets scan.
Every enabled check writes its own section to $GITHUB_STEP_SUMMARY, so a PR shows one
report with a heading per check, followed by a ## Result section that lists pass/fail/
skipped per check and the overall verdict. The job step itself fails if any enabled check
failed; a skipped check (disabled, or skillxray finding nothing to scan) never fails it.
The gate runs live on munzzyy/munzzyy: noslop and the secrets scan over the repo, zizmor over its workflow, and skillxray skipping cleanly because there's nothing skill-shaped to scan.
This repo also ships a small Python package, so you can run the exact same checks on your own machine before you open a PR, instead of finding out from a red CI run:
git clone https://github.com/munzzyy/ci-safety-gate
cd ci-safety-gate
pip install -e .
cd /path/to/your-repo
ci-safety-gate --local
(or, from inside this checkout with no install at all: python -m ci_safety_gate --local.)
--local runs the same commands action.yml's composite steps run — noslop and zizmor over
your tracked files, skillxray if it finds SKILL.md / skills/ / .claude, the bundled
secrets scan — against a directory you point it at (default .), hands the result to the
same evaluate_gate.py the action itself calls, and prints the same combined summary to your
terminal instead of $GITHUB_STEP_SUMMARY. Same verdict, same exit code.
Every action.yml input has a matching flag — --no-noslop, --zizmor-min-severity,
--secrets-exclude, and so on. Run ci-safety-gate --help for the full list. Defaults come
straight out of action.yml at run time rather than a hand-copied second set, so a version
bump or threshold change there shows up here for free.
A scanner that isn't installed fails loud instead of skipping quietly:
## zizmor (GitHub Actions security audit)
zizmor did not run (not installed): install with `pip install zizmor`, or re-run with
--install-missing to do it automatically.
Pass --install-missing and it runs the exact pip install action.yml's own install steps
run (same pins, same skillxray git ref) instead of just naming the command.
--localonly works from a checkout of this repo (git clone+pip install -e .), since it reads its defaults straight out of the realaction.ymlon disk instead of a bundled copy. A real, non-editable PyPI wheel would needaction.ymlshipped as package data for this to keep working — a follow-up for whenever this package is actually published, not solved yet.- The flags mirror
action.yml's inputs, but the literal commands each check runs (ci_safety_gate/checks.py) are still a second, hand-written copy of the bash inaction.yml— rewriting the composite action to call into this package would close that gap for good, but that's a bigger, riskier change than this pass makes.tests/test_checks.pyasserts every flagchecks.pybuilds is still literally present inaction.yml's own bash, so the two failing to match is a test failure, not a silent drift. --localuses whatever Python, noslop, zizmor, and skillxray are already on yourPATH(or installs them with--install-missing); it doesn't manage a separate pinnedpython-versionthe way the composite action does.
- It's an orchestrator, not a detector. The actual detection quality is whatever noslop, zizmor, and skillxray ship; this action just installs them, runs them the same way every time, and merges the output. A finding a bundled tool misses, this gate misses too.
- skillxray installs from a pinned git tag (
git+https://github.com/munzzyy/skillxray@v0.1.1), not from PyPI, because it isn't published there yet. Pointskillxray-refat a newer tag once one exists, or drop this once it's on PyPI. - The bundled secrets scan is deliberately small: eight high-precision patterns, no entropy analysis, no git-history scan. It's a floor, not a replacement for gitleaks or a real secret manager.
- zizmor's default here is
--offline, so its checks that need the GitHub API (like whether a pinned SHA still matches its tag) are off unless you setzizmor-offline: "false"and give the job a token.
The action's own result output is "pass" or "fail". The job step itself exits non-zero
on "fail", which is what actually fails your workflow; the output exists for a caller that
wants to branch on it instead.
See CONTRIBUTING.md. Every ${{ }} expression that touches repo content
has to go through an env: block, not a direct string interpolation; zizmor enforces this on
the action's own workflow.
MIT — free to use, change, and ship, commercial or not. See LICENSE.
If the gate caught something before it merged, sponsoring is what keeps it maintained.