semgrep --config=auto Turns main Red, No Code Changed
This article may contain affiliate links. Its content is not affected by advertising.
In short
semgrep ci --config=auto fetches the Semgrep registry's rules on every run, so with the repository completely unchanged, main can still turn red the day upstream adds a rule.
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.
Frequently asked questions
Q1Why did main turn red with no code changes at all?
semgrep ci --config=auto fetched the Semgrep registry's rules (1,074 measured) on every run. In July 2026, upstream added new supply-chain rules, and 41 findings got flagged as blocking with the repository untouched, turning main red.
Q2SEMGREP_RULES declared a rule set — why didn't it take effect?
The old setup declared five rule sets via env: SEMGREP_RULES, but the run command still passed --config=auto. Semgrep never reads that env declaration in that mode, so the full registry ran every time regardless of what was declared.
Q3Is the fix now fully deterministic?
No. Each p/* rule set is itself fetched from the registry at scan time and can still change upstream. Three new terraform rules from p/owasp-top-ten in fact surfaced during the migration itself.
Q4Was --config=auto removed entirely?
No. It was pulled out of the blocking gate, kept as a separate semgrep-discovery job that only runs on the weekly schedule, with continue-on-error: true, surfacing new rules as code scanning alerts instead.
Q5Why are three pnpm rules excluded?
pnpm-minimum-release-age and pnpm-block-exotic-sub-dependencies were false positives since pnpm 11 already enables that by default. pnpm-trust-policy re-applies to the whole lockfile even under --frozen-lockfile, so it was deliberately left out.
Environment verified
- semgrep/semgrep image 1.171.0 (pinned by digest) / actions/checkout v6.1.0
- Fixed in キミテラス-v2's CI on 2026-07-25 (the old setup had main red on and off through July 2026 whenever upstream added rules)
What this article is based on
- YAML file lines 27-45commit abb0ba6
- YAML file lines 31-96commit 48f38e3
- YAML file lines 98-120commit 48f38e3
- YAML file lines 9-32commit 48f38e3
- YAML filecommit 48f38e3
Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.