OpenSource CLI
The Plumber CLI allows you to analyze GitLab CI/CD pipelines from the command line for security and compliance issues like:
- Container images using mutable tags (
latest,dev) - Container images from untrusted registries
- Unprotected branches
- Hardcoded jobs not from includes/components
- Outdated includes/templates
- Forbidden version patterns (e.g.,
main,HEAD) - Missing required components or templates
This is useful for local testing, CI/CD integration, or automated compliance checks.
View on GitHubInstallation
brew tap getplumber/plumberbrew install plumberInstall a specific version:
brew install getplumber/plumber/plumber@0.1.72Info
Versioned formulas are keg-only. Use the full path (e.g., /usr/local/opt/plumber@0.1.72/bin/plumber) or run brew link plumber@0.1.72 to add it to your PATH.
mise use -g github:getplumber/plumberInfo
Requires mise activation in your shell, or run with mise exec -- plumber.
Linux (amd64)
curl -LO https://github.com/getplumber/plumber/releases/latest/download/plumber-linux-amd64chmod +x plumber-linux-amd64sudo mv plumber-linux-amd64 /usr/local/bin/plumberLinux (arm64)
curl -LO https://github.com/getplumber/plumber/releases/latest/download/plumber-linux-arm64chmod +x plumber-linux-arm64sudo mv plumber-linux-arm64 /usr/local/bin/plumbermacOS (Apple Silicon)
curl -LO https://github.com/getplumber/plumber/releases/latest/download/plumber-darwin-arm64chmod +x plumber-darwin-arm64sudo mv plumber-darwin-arm64 /usr/local/bin/plumbermacOS (Intel)
curl -LO https://github.com/getplumber/plumber/releases/latest/download/plumber-darwin-amd64chmod +x plumber-darwin-amd64sudo mv plumber-darwin-amd64 /usr/local/bin/plumberWindows (PowerShell)
Invoke-WebRequest -Uri https://github.com/getplumber/plumber/releases/latest/download/plumber-windows-amd64.exe -OutFile plumber.exeVerify checksum (optional):
curl -LO https://github.com/getplumber/plumber/releases/latest/download/checksums.txtsha256sum -c checksums.txt --ignore-missingdocker pull getplumber/plumber:latestRun analysis directly with Docker:
docker run --rm \ -e GITLAB_TOKEN=glpat-xxxx \ getplumber/plumber:latest analyze \ --gitlab-url https://gitlab.com \ --project mygroup/myprojectgit clone https://github.com/getplumber/plumber.gitcd plumbermake build# or: make install (builds and copies to /usr/local/bin/)Info
Requires Go 1.24+ and Make.
Quick Start
Generate a config file
Terminal window plumber config generateThis creates
.plumber.yamlwith default compliance rules. You can customize it later.Create and set your GitLab token
In GitLab, go to User Settings → Access Tokens (direct link) and create a Personal Access Token with
read_api+read_repositoryscopes. Project Access Tokens also work: create one inside your project under Settings → Access Tokens with the same scopes and at least Maintainer role.Caution
The token must belong to a user (or project bot) with Maintainer role (or higher) on the project to access branch protection settings and other project configurations.
Terminal window export GITLAB_TOKEN=glpat-xxxxRun analysis
Terminal window # If you're in a git repo with a GitLab remote, just run:# the gitlab url and projects are automatically detected from the .git# auto-detection requires the remote to be set to 'origin'plumber analyze# Or specify the project explicitly:plumber analyze --gitlab-url https://gitlab.com --project mygroup/myprojectPlumber auto-detects the GitLab URL and project from your git remote (requires remote to be set to
origin).Review results
Plumber reads your
.plumber.yamlconfig and outputs a compliance report. You can also store the output in JSON format with the--outputflag.
Command Reference
plumber analyze
The main command for analyzing GitLab CI/CD pipelines.
plumber analyze [flags]Flags
| Flag | Required | Default | Description |
|---|---|---|---|
--gitlab-url | No* | auto-detect | GitLab instance URL (e.g., https://gitlab.com) |
--project | No* | auto-detect | Project path (e.g., group/project) |
--config | No | .plumber.yaml | Path to configuration file |
--threshold | No | 100 | Minimum compliance % to pass (0-100) |
--branch | No | Project default | Branch to analyze |
--output | No | - | Write JSON results to file |
--pbom | No | - | Write PBOM (Pipeline Bill of Materials) to file |
--pbom-cyclonedx | No | - | Write PBOM in CycloneDX SBOM format |
--print | No | true | Print text output to stdout |
--mr-comment | No | false | Post/update a compliance comment on the merge request (MR pipelines only; requires api scope) |
--badge | No | false | Create/update a Plumber compliance badge on the project (requires api scope; only runs on default branch) |
--controls | No | - | Run only listed controls (comma-separated). Cannot be used with --skip-controls |
--skip-controls | No | - | Skip listed controls (comma-separated). Cannot be used with --controls |
--fail-warnings | No | false | Treat configuration warnings (unknown keys) as errors (exit 2) |
--verbose, -v | No | false | Enable verbose/debug output |
Info
* Auto-detected from git remote (requires origin) if not specified. Supports both SSH and HTTPS remote URLs.
* You can always override with --gitlab-url and --project
Environment Variables
| Variable | Required | Description |
|---|---|---|
GITLAB_TOKEN | Yes | GitLab API token with read_api + read_repository scopes (from a Maintainer or higher). Use api scope instead if --mr-comment or --badge is enabled. |
PLUMBER_NO_UPDATE_CHECK | No | Set to any value (e.g., 1) to disable the automatic version check. |
Automatic Version Check
When running locally, Plumber checks GitHub for newer releases on every invocation and prints an upgrade notice if one is available. The check runs asynchronously and has a 3-second timeout, so it never slows down the analysis.
The check is automatically skipped when:
- Running in CI environments (
CIorGITLAB_CIenvironment variables are set) - Using a development build (version is
dev)
To disable it manually:
export PLUMBER_NO_UPDATE_CHECK=1Exit Codes
| Code | Meaning |
|---|---|
0 | Passed (compliance ≥ threshold) |
1 | Compliance failure (compliance < threshold) |
2 | Runtime error (config error, network failure, missing token, etc.) |
plumber config generate
Generate a default .plumber.yaml configuration file.
plumber config generate [flags]| Flag | Default | Description |
|---|---|---|
--output, -o | .plumber.yaml | Output file path |
--force, -f | false | Overwrite existing file |
Examples:
# Generate default configplumber config generate
# Custom filenameplumber config generate --output my-plumber.yaml
# Overwrite existing fileplumber config generate --forceplumber config view
Display a clean, human-readable view of the effective configuration without comments.
plumber config view [flags]| Flag | Default | Description |
|---|---|---|
--config, -c | .plumber.yaml | Path to configuration file |
--no-color | false | Disable colorized output |
Booleans are colorized for quick scanning: true in green, false in red. Color is automatically disabled when piping output.

Examples:
# View the default .plumber.yamlplumber config view
# View a specific config fileplumber config view --config custom-plumber.yaml
# View without colors (for piping or scripts)plumber config view --no-colorplumber config validate
Validate a configuration file for correctness. Detects unknown control names and sub-keys with typo suggestions using fuzzy matching.
plumber config validate [flags]| Flag | Default | Description |
|---|---|---|
--config, -c | .plumber.yaml | Path to configuration file |
--fail-warnings | false | Treat configuration warnings as errors (exit 2) |
Warnings are printed to stderr so they don’t interfere with scripted output. Use --fail-warnings to exit with code 2 when warnings are found (useful in CI).
Examples:
# Validate the default .plumber.yamlplumber config validate
# Validate a specific config fileplumber config validate --config custom-plumber.yaml
# Fail on warnings (for CI pipelines)plumber config validate --fail-warningsSample output with typos:
Configuration validation warnings: - Unknown control in .plumber.yaml: "containerImageMustNotUseForbiddenTag". Did you mean "containerImageMustNotUseForbiddenTags"? - Unknown key "tag" in control "containerImageMustNotUseForbiddenTags". Did you mean "tags"? - Unknown key "allowForcePushes" in control "branchMustBeProtected". Did you mean "allowForcePush"?Usage Examples
Save JSON Output
docker run --rm \ -e GITLAB_TOKEN=glpat-xxxx \ -v $(pwd):/output \ getplumber/plumber:latest analyze \ --gitlab-url https://gitlab.com \ --project mygroup/myproject \ --branch main \ --config /.plumber.yaml \ --threshold 100 \ --output /output/results.jsonSelf-Hosted GitLab
plumber analyze \ --gitlab-url https://gitlab.example.com \ --project mygroup/myproject \ --branch develop \ --config .plumber.yaml \ --threshold 80Silent Mode (JSON Only)
plumber analyze \ --gitlab-url https://gitlab.com \ --project mygroup/myproject \ --config .plumber.yaml \ --threshold 100 \ --output results.json \ --print falseExample Output
The CLI output is color-coded in your terminal for easy scanning — green for passing controls, red for failures.
Tip
When using --output, results are saved as JSON for programmatic access and CI/CD integration.

Pipeline Bill of Materials (PBOM) & CycloneDX
Plumber can generate a PBOM — a complete inventory of all dependencies in your CI/CD pipeline (container images, components, templates, includes). Two formats are available:
Native PBOM (detailed, pipeline-specific):
plumber analyze --pbom pbom.jsonCycloneDX SBOM (standard format for security tool integration):
plumber analyze --pbom-cyclonedx pipeline-sbom.jsonThe CycloneDX output follows the CycloneDX 1.5 specification and is compatible with tools like Grype, Trivy, and Dependency-Track. When using the GitLab CI component, the CycloneDX file is automatically uploaded as a GitLab CycloneDX report.
Info
CI/CD components and templates do not have CVEs in public vulnerability databases. The PBOM is primarily an inventory and compliance tool — it tells you what’s in your pipeline, not whether those items have known vulnerabilities. For image vulnerability scanning, use trivy image or grype directly on the images.
Configuration
Plumber uses a .plumber.yaml configuration file to customize checks.
Available Controls
Plumber includes compliance controls covering CI/CD configuration, repository settings, and access management. Each can be enabled/disabled and customized. When a control detects a violation, it creates an Issue (e.g., ISSUE-101) with a direct link to its documentation page.
| Control | Issues | Description |
|---|---|---|
| Container images must not use forbidden tags | ISSUE-102 | Flags latest, dev, and other mutable tags. Can enforce digest pinning for all images |
| Container images must be pinned by digest | ISSUE-103 | Ensures images use SHA256 digest pinning |
| Container images must come from authorized sources | ISSUE-101 | Ensures images come from trusted registries |
| Branch must be protected | ISSUE-501 ISSUE-505 | Verifies critical branches have proper protection |
| Pipeline must not include hardcoded jobs | ISSUE-401 | Detects jobs defined directly instead of from includes |
| Includes must be up to date | ISSUE-403 | Checks if included templates have newer versions |
| Includes must not use forbidden versions | ISSUE-404 | Prevents mutable version references like main, HEAD |
| Pipeline must include component | ISSUE-408 ISSUE-409 | Ensures required CI/CD components are included; detects overridden jobs |
| Pipeline must include template | ISSUE-405 ISSUE-406 | Ensures required templates are included; detects overridden jobs |
| Pipeline must not enable debug trace | ISSUE-203 | Detects CI_DEBUG_TRACE/CI_DEBUG_SERVICES leaking secrets in job logs |
| Pipeline must not use unsafe variable expansion | ISSUE-204 | Detects user-controlled variables in shell re-interpretation contexts (OWASP CICD-SEC-1) |
| Security jobs must not be weakened | ISSUE-410 | Detects security scanning jobs neutralized by allow_failure, rules: overrides, or when: manual (OWASP CICD-SEC-4) |
| Pipeline must not execute unverified scripts | ISSUE-411 | Detects curl | bash, wget | sh, and download-then-execute patterns without integrity verification (OWASP CICD-SEC-3) |
Unsafe Variable Expansion Detection
Detects user-controlled CI variables (MR title, commit message, branch name) passed to commands that re-interpret their input as shell code. This is OWASP CICD-SEC-1.
GitLab sets CI variables as environment variables. The shell does not re-parse expanded values for command substitution, so normal usage is safe. Only commands that re-interpret their arguments as code are flagged:
Flagged (re-interpretation contexts):
eval "$CI_COMMIT_BRANCH"sh -c "$CI_MERGE_REQUEST_TITLE"bash -c "$CI_COMMIT_MESSAGE"source <(echo "$CI_COMMIT_REF_NAME")echo "$CI_COMMIT_BRANCH" | xargs shNot flagged (safe, the shell doesn’t re-parse env var values):
echo $CI_COMMIT_BRANCHcurl -d "$CI_MERGE_REQUEST_TITLE" https://...git checkout $CI_COMMIT_REF_NAMEprintf '%s' "$CI_COMMIT_MESSAGE"Allowing specific patterns
Some sh -c or bash -c usages are legitimate (e.g., Helm deploys, Terraform workspaces). Use allowedPatterns (regex) to suppress those findings. Each pattern is matched against the full script line.
pipelineMustNotUseUnsafeVariableExpansion: enabled: true dangerousVariables: - CI_MERGE_REQUEST_TITLE - CI_COMMIT_MESSAGE - CI_COMMIT_REF_NAME - CI_COMMIT_REF_SLUG - CI_COMMIT_BRANCH allowedPatterns: - "helm.*--set.*\\$CI_" - "terraform workspace select.*\\$CI_" - "docker build.*--build-arg.*\\$CI_" - "aws s3 sync.*\\$CI_" - "make deploy.*\\$CI_"For example, the script line sh -c "helm upgrade myapp . --set image.tag=$CI_COMMIT_SHA" would normally be flagged, but the pattern helm.*--set.*\\$CI_ allows it.
Info
Escape $ as \\$ and {/} as \\{/\\} in patterns. Only direct variable names are detected. Indirect aliasing (variables: { B: $CI_COMMIT_BRANCH } then sh -c $B) is not tracked.
Security Job Weakening Detection
GitLab lets you override any property of an included job. This means someone can include a security template (SAST, Secret Detection, Container Scanning, Dependency Scanning, DAST, License Scanning) but silently neutralize it. The pipeline still looks compliant, but the scanning is disabled. Maps to OWASP CICD-SEC-4 (Poisoned Pipeline Execution).
This control detects three weakening patterns, each a separate sub-control you can toggle independently:
allowFailureMustBeFalse(default: off, opt-in): Detectsallow_failure: true. Off by default because GitLab templates ship with this setting.rulesMustNotBeRedefined(default: on): Detectsrules:overrides withwhen: neverorwhen: manual.whenMustNotBeManual(default: on): Detectswhen: manualset at job level.
Flagged: security jobs weakened
include: - template: Security/SAST.gitlab-ci.yml
semgrep-sast: allow_failure: true # failures silently ignored
secret_detection: rules: - when: never # job will never run
container_scanning: when: manual # requires manual triggerConfiguration
Security jobs are identified by matching job names against securityJobPatterns (wildcards supported):
securityJobsMustNotBeWeakened: enabled: true securityJobPatterns: - "*-sast" - "secret_detection" - "container_scanning" - "*_dependency_scanning" - "gemnasium-*" - "dast" - "dast_*" - "license_scanning" allowFailureMustBeFalse: enabled: false rulesMustNotBeRedefined: enabled: true whenMustNotBeManual: enabled: trueUnverified Script Execution Detection
Detects CI/CD jobs that download and immediately execute scripts from the internet without integrity verification. This is a well-documented supply chain attack vector: an attacker who compromises the remote URL can serve a modified script that exfiltrates secrets. Maps to OWASP CICD-SEC-3 (Dependency Chain Abuse) and CICD-SEC-8 (Ungoverned Usage of 3rd Party Services).
Detected patterns:
- Direct pipe to shell:
curl ... | bash,wget ... | sh,curl ... | python, etc. - Download-and-execute:
curl -o script.sh ... && bash script.sh - Download-redirect-execute:
curl ... > install.sh; sh install.sh
Lines that include checksum verification (e.g., sha256sum, gpg --verify) between the download and execution are automatically excluded.
Configuration:
pipelineMustNotExecuteUnverifiedScripts: enabled: true trustedUrls: [] # - https://internal-artifacts.example.com/*Add trusted URL patterns to trustedUrls (supports wildcards) to suppress findings for known-good sources.
Issues
Every violation detected by Plumber is identified as an Issue following the pattern ISSUE-XXXX. Each issue links to a dedicated documentation page with:
- Description of the problem
- Impact on your security posture
- How to fix with before/after configuration examples
- Tips for best practices
Issues are displayed in the CLI output, MR comments, and the summary table. Click any issue to view its full documentation.
| Issue | Title |
|---|---|
| ISSUE-101 | Untrusted image source |
| ISSUE-102 | Forbidden container image tag |
| ISSUE-103 | Container image not pinned by digest |
| ISSUE-201 | Unprotected variable |
| ISSUE-202 | Unmasked variable |
| ISSUE-203 | Pipeline enables CI debug trace |
| ISSUE-204 | Unsafe variable expansion |
| ISSUE-301 | Secret leak in pipeline configuration |
| ISSUE-401 | Hardcoded job |
| ISSUE-402 | Forbidden override of job |
| ISSUE-403 | Outdated template |
| ISSUE-404 | Forbidden include version |
| ISSUE-405 | Missing required template |
| ISSUE-406 | Forbidden override of required template |
| ISSUE-501 | Branch protection missing |
| ISSUE-601 | Missing security policy source on project |
| ISSUE-502 | Merge request approval rule is below the minimum level of approvals required |
| ISSUE-503 | Merge request approval settings are not compliant |
| ISSUE-504 | No merge request approval rule covering all protected branches |
| ISSUE-407 | Invalid pipeline composition |
| ISSUE-505 | Branch protection configuration not compliant |
| ISSUE-506 | Merge request settings are not compliant |
| ISSUE-408 | Missing required component |
| ISSUE-409 | Forbidden override of required component |
| ISSUE-507 | Members’ role quotas are not respected for projects |
| ISSUE-508 | Members’ role quotas are not respected for groups |
| ISSUE-410 | Security job weakened |
| ISSUE-411 | Unverified script execution |
Example Configuration
Tip
See the full configuration reference for all options.
version: "1.0"
controls: containerImageMustNotUseForbiddenTags: enabled: true tags: - latest - dev - main # When true, ALL images must be pinned by digest (e.g., alpine@sha256:...) # Takes precedence over tags list — even version tags like alpine:3.19 will be flagged containerImagesMustBePinnedByDigest: false
containerImageMustComeFromAuthorizedSources: enabled: true trustDockerHubOfficialImages: true trustedUrls: - $CI_REGISTRY_IMAGE:* - registry.gitlab.com/security-products/*
branchMustBeProtected: enabled: true defaultMustBeProtected: true namePatterns: - main - release/* allowForcePush: false minMergeAccessLevel: 30 # Developer minPushAccessLevel: 40 # Maintainer
pipelineMustNotIncludeHardcodedJobs: enabled: true
includesMustBeUpToDate: enabled: true
includesMustNotUseForbiddenVersions: enabled: true forbiddenVersions: - latest - "~latest" - main - master - HEAD defaultBranchIsForbiddenVersion: false
pipelineMustIncludeComponent: enabled: false # Disabled by default - enable and configure for your org # Expression syntax (use one, not both): # required: components/sast/sast AND components/secret-detection/secret-detection # Array syntax (OR of ANDs): # requiredGroups: # - ["components/sast/sast", "components/secret-detection/secret-detection"] # - ["your-org/full-security/full-security"]
pipelineMustIncludeTemplate: enabled: false # Disabled by default - enable and configure for your org # Expression syntax (use one, not both): # required: templates/go/go AND templates/trivy/trivy # Array syntax (OR of ANDs): # requiredGroups: # - ["templates/go/go", "templates/trivy/trivy"] # - ["templates/full-go-pipeline"]
# Detect debug trace variables that leak secrets in job logs pipelineMustNotEnableDebugTrace: enabled: true forbiddenVariables: - CI_DEBUG_TRACE - CI_DEBUG_SERVICES
# Detect user-controlled variables in shell re-interpretation contexts (eval, sh -c, etc.) # Safe: echo $CI_COMMIT_BRANCH. Dangerous: eval "deploy $CI_COMMIT_BRANCH" pipelineMustNotUseUnsafeVariableExpansion: enabled: true dangerousVariables: - CI_MERGE_REQUEST_TITLE - CI_MERGE_REQUEST_DESCRIPTION - CI_COMMIT_MESSAGE - CI_COMMIT_TITLE - CI_COMMIT_TAG_MESSAGE - CI_COMMIT_REF_NAME - CI_COMMIT_REF_SLUG - CI_COMMIT_BRANCH - CI_MERGE_REQUEST_SOURCE_BRANCH_NAME - CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME # Regex patterns to allow specific script lines (escape $ as \\$) allowedPatterns: - "helm.*--set.*\\$CI_" - "terraform workspace select.*\\$CI_" - "docker build.*--build-arg.*\\$CI_"
# Detect security scanning jobs that have been silently weakened securityJobsMustNotBeWeakened: enabled: true securityJobPatterns: - "*-sast" - "secret_detection" - "container_scanning" - "*_dependency_scanning" - "gemnasium-*" - "dast" - "dast_*" - "license_scanning" allowFailureMustBeFalse: enabled: false # opt-in (GitLab templates ship with allow_failure: true) rulesMustNotBeRedefined: enabled: true whenMustNotBeManual: enabled: true
# Detect unverified script downloads and execution (curl|bash, wget|sh) pipelineMustNotExecuteUnverifiedScripts: enabled: true trustedUrls: [] # - https://internal-artifacts.example.com/*Info
Controls 7 and 8 support two syntax options for defining requirements (use one, not both):
- Expression syntax (
required): A natural boolean expression usingAND,OR, and parentheses.ANDbinds tighter thanOR. - Array syntax (
requiredGroups): A list of groups using “OR of ANDs” logic — outer array = OR, inner array = AND.
Selective Control Execution
You can run or skip specific controls using their YAML key names from .plumber.yaml. This is useful for iterative debugging or targeted CI checks.
Run only specific controls:
# Only check image tags and branch protectionplumber analyze --controls containerImageMustNotUseForbiddenTags,branchMustBeProtectedSkip specific controls:
# Run everything except branch protectionplumber analyze --skip-controls branchMustBeProtectedControls not selected are reported as skipped in the output. The --controls and --skip-controls flags are mutually exclusive.
Valid control names
| Control Name |
|---|
branchMustBeProtected |
containerImageMustComeFromAuthorizedSources |
containerImageMustNotUseForbiddenTags |
includesMustBeUpToDate |
includesMustNotUseForbiddenVersions |
pipelineMustIncludeComponent |
pipelineMustIncludeTemplate |
pipelineMustNotEnableDebugTrace |
pipelineMustNotExecuteUnverifiedScripts |
pipelineMustNotIncludeHardcodedJobs |
pipelineMustNotUseUnsafeVariableExpansion |
securityJobsMustNotBeWeakened |
GitLab Integration
Plumber integrates directly with GitLab to provide visual compliance feedback where your team works.
Merge Request Comments
Automatically post compliance summaries on merge requests to catch issues before they’re merged.
plumber analyze --mr-commentOr via the GitLab CI component:
include: - component: gitlab.com/getplumber/plumber/plumber@v0.1.29 inputs: mr_comment: true # Requires api scope on token
Features:
- Shows compliance badge with pass/fail status
- Lists all controls with individual compliance percentages
- Details specific issues found with job names and image references
- Automatically updates on each pipeline run (no duplicate comments)
Caution
Token requirement: The api scope is required (not read_api) to create/update MR comments. The --mr-comment flag only works in merge request pipelines (CI_MERGE_REQUEST_IID must be set).
Project Badges
Display a live compliance badge on your project’s overview page.
plumber analyze --badgeOr via the GitLab CI component:
include: - component: gitlab.com/getplumber/plumber/plumber@v0.1.29 inputs: badge: true # Requires api scope on token
Features:
- Shows current compliance percentage
- Green when compliance meets threshold, red when below
- Only updates on default branch pipelines (not on MRs or feature branches)
- Badge appears in GitLab’s “Project information” section
Caution
Token requirement: The api scope is required (not read_api) and Maintainer role to manage project badges.
Troubleshooting
| Issue | Solution |
|---|---|
GITLAB_TOKEN environment variable is required | Set the GITLAB_TOKEN environment variable with a valid GitLab token |
401 Unauthorized | Token needs read_api + read_repository scopes, from a Maintainer or higher |
403 Forbidden on MR settings | Expected on non-Premium GitLab; continues without that data |
403 Forbidden on MR comment | Token needs api scope (not read_api) when --mr-comment is enabled |
403 Forbidden on badge | Token needs api scope (not read_api) when --badge is enabled |
404 Not Found | Verify the project path and GitLab URL are correct |
| MR comment not posted | --mr-comment only works in merge request pipelines (CI_MERGE_REQUEST_IID must be set) |
| Badge not created/updated | Token needs api scope and Maintainer role (or higher) on the project |
| Configuration file not found | Ensure the path to .plumber.yaml is correct (use absolute path in Docker) |