Conclusion
semgrep ci --config=auto fetches the Semgrep registry's community rules (1,074 measured) on every single run, so even with zero code changes in the repository, CI's main branch can turn red on the exact day upstream adds a new rule. The old setup declared five rule sets via the SEMGREP_RULES environment variable, but the run command itself still passed --config=auto, so that declaration was never read — the full registry ran every time. The fix was to list the rule sets explicitly with --config=p/xxx and pin the Semgrep container image by digest.
Symptom
One of our production repositories' security.yml uses Semgrep for SAST (static analysis). Before the fix (as of abb0ba6), the config looked like this:
semgrep:
name: SAST (Semgrep)
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v6
- run: semgrep ci --config=auto --sarif --output=semgrep.sarif
env:
SEMGREP_RULES: >-
p/typescript
p/javascript
p/nodejs
p/owasp-top-ten
p/security-audit
At a glance, this looks like it's scoped to five rule sets via SEMGREP_RULES. But the actual semgrep ci invocation passes --config=auto, and once --config=auto is set, Semgrep fetches and runs the Semgrep registry's community rules on every invocation (1,074 measured). The SEMGREP_RULES environment variable is never read in this mode. The container image was also left unpinned as semgrep/semgrep (i.e., latest).
In July 2026, the Semgrep registry added new supply-chain rules, and that alone caused 41 findings to be flagged as blocking, turning main persistently red. Not a single line of repository code had changed.
Cause
Two things compounded here.
-
--config=autosilently overrode theSEMGREP_RULESdeclaration. The five declared rule sets were never actually running — the full registry (1,074 rules) was in scope every time. The declaration and the actual behavior had drifted apart without anyone noticing. -
Semgrep OSS has no severity filter. Everything detected is treated as blocking, so the gate's pass/fail outcome is determined entirely by which rules get run. Since
--config=autodepends on the live contents of the Semgrep registry, the gate's outcome can flip the moment upstream adds or changes a rule. The container image being unpinned (latest) meant Semgrep's own version bumps could shift behavior too.
Together, these two produced a structure that code review can't catch: main can turn red purely because upstream added a rule, with the repository itself completely unchanged.
The fix
The fix came in three parts.
① List the rule sets explicitly and pin the engine by digest.
semgrep:
name: SAST (Semgrep)
runs-on: ubuntu-latest
container:
image: semgrep/semgrep@sha256:bdf7013b2c3634a487671158da77c554f531742326b543a9464d2adf6c433ac8 # 1.171.0
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- run: >-
semgrep ci --sarif --output=semgrep.sarif
--config=p/typescript
--config=p/javascript
--config=p/nodejs
--config=p/owasp-top-ten
--config=p/security-audit
--config=p/github-actions
--exclude-rule=package_managers.pnpm.pnpm-missing-minimum-release-age.pnpm-minimum-release-age
--exclude-rule=package_managers.pnpm.pnpm-block-exotic-sub-dependencies.pnpm-block-exotic-sub-dependencies
--exclude-rule=package_managers.pnpm.pnpm-trust-policy.pnpm-trust-policy
--exclude-rule=terraform.gcp.security.gcp-cloud-storage-logging.gcp-cloud-storage-logging
--config=auto is gone, replaced by six explicit --config=p/xxx entries. The container image moved from latest to a pinned digest. p/github-actions was newly added to keep continuously checking that SHA pinning on uses: hasn't regressed.
Each of the four rules excluded via --exclude-rule has a reason recorded as a code comment. pnpm-minimum-release-age and pnpm-block-exotic-sub-dependencies were "false positives against settings pnpm 11 already enables by default"; pnpm-trust-policy was "deliberately not adopted, because it re-applies to the entire existing lockfile even under --frozen-lockfile, so CI could fail purely from an upstream change with the repository untouched"; the one terraform rule was "a real finding, tracked separately in another issue." Exclusions here were made with a stated reason each time, not silently appended.
② Move --config=auto into a weekly, non-blocking job.
semgrep-discovery:
name: SAST Discovery (Semgrep auto, non-blocking)
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
continue-on-error: true
container:
image: semgrep/semgrep@sha256:bdf7013b2c3634a487671158da77c554f531742326b543a9464d2adf6c433ac8 # 1.171.0
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- run: semgrep ci --config=auto --sarif --output=semgrep-auto.sarif
continue-on-error: true
The broad net that --config=auto casts wasn't thrown away. It was pulled out of the blocking gate and moved into a job that only runs once a week on schedule and is non-blocking. Findings from this job no longer stop main — they persist as code scanning alerts instead.
③ SHA-pin every uses:. In the same commit, all uses: references under .github/workflows were converted to 40-character SHAs with a version comment — 35 in total (13 in security.yml, 22 in ci.yml). actions/dependency-review-action@v5 had been pinned to a branch rather than a tag, making it even more mutable than the other tag references.
Preventing a repeat
This kind of structure comes back unless someone checks whether a rule set that looks scoped is actually being overridden by another flag on the same run command. Declaring a rule set via env is never read once semgrep ci runs with --config=auto — scoping has to be passed as a --config flag instead. And when a tool with no severity filter is used as a blocking gate, where the rule set is fetched from (a live registry, as with auto, versus a fixed set like p/xxx) becomes a variable that can flip the gate's outcome even without any code change. Explicitly listing p/xxx sets still doesn't make things fully deterministic — like auto, their contents are fetched from the registry at scan time. With that in mind, deciding up front how much to allow through the blocking gate versus routing to discovery makes it easier to avoid this same flavor of persistent red.
Originally published at tech.rebounder.jp.
Top comments (0)