@mrciphersmith/keryx 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/cli.js +7310 -2471
  2. package/dist/core.js +116 -10
  3. package/package.json +1 -1
  4. package/src/gdskills/bundled/install-manifest.json +349 -2
  5. package/src/gdskills/bundled/rules/core/model-selection.mdc +18 -0
  6. package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.md +1 -1
  7. package/src/gdskills/bundled/skills/planning/brainstorm/SKILL.md +1 -1
  8. package/src/gdskills/bundled/skills/planning/interviewer/SKILL.md +1 -1
  9. package/src/gdskills/bundled/skills/quality/deploy/SKILL.md +1 -1
  10. package/src/gdskills/bundled/skills/review/review-jev-comments/SKILL.md +184 -0
  11. package/src/gdskills/bundled/skills/review/review-jev-contract/SKILL.md +193 -0
  12. package/src/gdskills/bundled/skills/review/review-jev-docs/SKILL.md +189 -0
  13. package/src/gdskills/bundled/skills/review/review-jev-risk/SKILL.md +190 -0
  14. package/src/gdskills/bundled/skills/review/review-jev-scenarios/SKILL.md +187 -0
  15. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +88 -15
  16. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +4 -4
  17. package/src/gdskills/bundled/stacks/c-cpp/agent-refs.json +4 -0
  18. package/src/gdskills/bundled/stacks/c-cpp/governance/eval.json +1777 -0
  19. package/src/gdskills/bundled/stacks/c-cpp/governance/scout.json +31 -0
  20. package/src/gdskills/bundled/stacks/c-cpp/pack.json +42 -0
  21. package/src/gdskills/bundled/stacks/c-cpp/rules/coding-style.mdc +80 -0
  22. package/src/gdskills/bundled/stacks/c-cpp/rules/patterns.mdc +87 -0
  23. package/src/gdskills/bundled/stacks/c-cpp/rules/security.mdc +90 -0
  24. package/src/gdskills/bundled/stacks/c-cpp/rules/testing.mdc +83 -0
  25. package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-build-fix/SKILL.md +153 -0
  26. package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-build-fix/evals.json +74 -0
  27. package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-code-review/SKILL.md +132 -0
  28. package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-code-review/evals.json +73 -0
  29. package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-implementation/SKILL.md +151 -0
  30. package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-implementation/evals.json +74 -0
  31. package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-testing/SKILL.md +152 -0
  32. package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-testing/evals.json +74 -0
  33. package/src/gdskills/bundled/stacks/ci-github-gitlab/agent-refs.json +4 -0
  34. package/src/gdskills/bundled/stacks/ci-github-gitlab/governance/eval.json +1295 -0
  35. package/src/gdskills/bundled/stacks/ci-github-gitlab/governance/scout.json +26 -0
  36. package/src/gdskills/bundled/stacks/ci-github-gitlab/pack.json +41 -0
  37. package/src/gdskills/bundled/stacks/ci-github-gitlab/rules/patterns.mdc +77 -0
  38. package/src/gdskills/bundled/stacks/ci-github-gitlab/rules/security.mdc +144 -0
  39. package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-build-fix/SKILL.md +121 -0
  40. package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-build-fix/evals.json +73 -0
  41. package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-code-review/SKILL.md +139 -0
  42. package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-code-review/evals.json +73 -0
  43. package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-implementation/SKILL.md +147 -0
  44. package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-implementation/evals.json +74 -0
  45. package/src/gdskills/bundled/stacks/docker-k8s-terraform/agent-refs.json +4 -0
  46. package/src/gdskills/bundled/stacks/docker-k8s-terraform/governance/eval.json +865 -0
  47. package/src/gdskills/bundled/stacks/docker-k8s-terraform/governance/scout.json +16 -0
  48. package/src/gdskills/bundled/stacks/docker-k8s-terraform/pack.json +46 -0
  49. package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/coding-style.mdc +74 -0
  50. package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/patterns.mdc +81 -0
  51. package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/security.mdc +146 -0
  52. package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/testing.mdc +61 -0
  53. package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-build-fix/SKILL.md +151 -0
  54. package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-build-fix/evals.json +74 -0
  55. package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-review/SKILL.md +135 -0
  56. package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-review/evals.json +76 -0
  57. package/src/gdskills/bundled/stacks/php-laravel/agent-refs.json +4 -0
  58. package/src/gdskills/bundled/stacks/php-laravel/governance/eval.json +1829 -0
  59. package/src/gdskills/bundled/stacks/php-laravel/governance/scout.json +33 -0
  60. package/src/gdskills/bundled/stacks/php-laravel/pack.json +41 -0
  61. package/src/gdskills/bundled/stacks/php-laravel/rules/coding-style.mdc +82 -0
  62. package/src/gdskills/bundled/stacks/php-laravel/rules/patterns.mdc +80 -0
  63. package/src/gdskills/bundled/stacks/php-laravel/rules/security.mdc +80 -0
  64. package/src/gdskills/bundled/stacks/php-laravel/rules/testing.mdc +82 -0
  65. package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-build-fix/SKILL.md +143 -0
  66. package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-build-fix/evals.json +74 -0
  67. package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-code-review/SKILL.md +126 -0
  68. package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-code-review/evals.json +76 -0
  69. package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-implementation/SKILL.md +140 -0
  70. package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-implementation/evals.json +75 -0
  71. package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-testing/SKILL.md +124 -0
  72. package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-testing/evals.json +74 -0
  73. package/src/gdskills/bundled/stacks/ruby-rails/agent-refs.json +4 -0
  74. package/src/gdskills/bundled/stacks/ruby-rails/governance/eval.json +1673 -0
  75. package/src/gdskills/bundled/stacks/ruby-rails/governance/scout.json +33 -0
  76. package/src/gdskills/bundled/stacks/ruby-rails/pack.json +42 -0
  77. package/src/gdskills/bundled/stacks/ruby-rails/rules/coding-style.mdc +69 -0
  78. package/src/gdskills/bundled/stacks/ruby-rails/rules/patterns.mdc +93 -0
  79. package/src/gdskills/bundled/stacks/ruby-rails/rules/security.mdc +90 -0
  80. package/src/gdskills/bundled/stacks/ruby-rails/rules/testing.mdc +89 -0
  81. package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-build-fix/SKILL.md +143 -0
  82. package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-build-fix/evals.json +73 -0
  83. package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-code-review/SKILL.md +134 -0
  84. package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-code-review/evals.json +71 -0
  85. package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-implementation/SKILL.md +141 -0
  86. package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-implementation/evals.json +72 -0
  87. package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-testing/SKILL.md +125 -0
  88. package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-testing/evals.json +72 -0
  89. package/src/gdskills/bundled/stacks/sql-db/agent-refs.json +4 -0
  90. package/src/gdskills/bundled/stacks/sql-db/governance/eval.json +1829 -0
  91. package/src/gdskills/bundled/stacks/sql-db/governance/scout.json +30 -0
  92. package/src/gdskills/bundled/stacks/sql-db/pack.json +40 -0
  93. package/src/gdskills/bundled/stacks/sql-db/rules/coding-style.mdc +69 -0
  94. package/src/gdskills/bundled/stacks/sql-db/rules/patterns.mdc +134 -0
  95. package/src/gdskills/bundled/stacks/sql-db/rules/security.mdc +74 -0
  96. package/src/gdskills/bundled/stacks/sql-db/rules/testing.mdc +83 -0
  97. package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-build-fix/SKILL.md +147 -0
  98. package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-build-fix/evals.json +72 -0
  99. package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-code-review/SKILL.md +132 -0
  100. package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-code-review/evals.json +73 -0
  101. package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-implementation/SKILL.md +153 -0
  102. package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-implementation/evals.json +77 -0
  103. package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-testing/SKILL.md +129 -0
  104. package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-testing/evals.json +73 -0
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "query": "Use when authoring or extending a GitHub Actions workflow (.github/workflows/*.yml) or a GitLab CI pipeline (.gitlab-ci.yml) -- trigger and job design, reusable workflows/templates, caching, least-privilege permissions, and safe handling of untrusted pull-request/merge-request input.",
4
+ "decision": "create",
5
+ "topMatch": "ci-github-gitlab/ci-pipeline-code-review (0.57, raw tool decision: use)",
6
+ "recordedAt": "2026-09-25T15:30:00.000Z",
7
+ "skillName": "ci-pipeline-implementation",
8
+ "justification": "keryx skills scout --candidate <own-dir> scored the raw top match as this pack's own ci-pipeline-code-review (overlap 0.57, above the 0.55 'use' threshold) purely on shared CI/permissions/pull-request vocabulary -- but ci-pipeline-code-review is a read-only review skill (no edits, findings + fix direction only) and cannot substitute for authoring/extending a workflow file, which requires actually writing YAML and applying a fix. No skill outside this new pack scored above the fork threshold (nothing in ts-js-node/go/python/docker-k8s-terraform authors GitHub Actions or GitLab CI YAML specifically), so this is a genuine create, not a substitute for an existing implement-category skill."
9
+ },
10
+ {
11
+ "query": "Use when reviewing a GitHub Actions workflow (.github/workflows/*.yml) or GitLab CI pipeline (.gitlab-ci.yml) change for security and structural risk -- pull_request_target combined with untrusted checkout, tag-pinned third-party actions, missing least-privilege permissions, script injection via unsanitized event/variable interpolation, and unprotected access to deploy secrets. Read-only, no edits.",
12
+ "decision": "create",
13
+ "topMatch": "ci-github-gitlab/ci-pipeline-implementation (0.37, raw tool decision: fork)",
14
+ "recordedAt": "2026-09-25T15:30:05.000Z",
15
+ "skillName": "ci-pipeline-code-review",
16
+ "justification": "keryx skills scout --candidate <own-dir> scored the raw top match as this pack's own ci-pipeline-implementation (overlap 0.37, above the 0.3 fork threshold) on shared CI/permissions/action vocabulary -- but ci-pipeline-implementation authors and edits workflow files, while this skill is strictly read-only (findings + fix direction, never an edit). The next-nearest matches outside this pack, docker-k8s-terraform's own review skill (0.17) and ts-js-node/nodejs-code-review (0.16), review Dockerfiles/Kubernetes manifests and JS/TS source respectively -- neither carries GitHub Actions/GitLab CI-specific vocabulary (pull_request_target, actions/checkout pinning, GITHUB_TOKEN permissions, protected GitLab variables), so this is a genuine create, not an adaptation of an existing review skill's content."
17
+ },
18
+ {
19
+ "query": "Use when a GitHub Actions workflow run or GitLab CI pipeline is failing at the pipeline-configuration level -- YAML syntax errors, an invalid trigger/job key, a missing or misscoped permissions block breaking a step, a job failing because a protected variable is unavailable on its branch, or a broken needs:/rules:/dependency graph -- with the smallest root-cause fix to the config itself, not the application code it builds.",
20
+ "decision": "create",
21
+ "topMatch": "ci-github-gitlab/ci-pipeline-implementation (0.28)",
22
+ "recordedAt": "2026-09-25T15:30:10.000Z",
23
+ "skillName": "ci-pipeline-build-fix",
24
+ "justification": "keryx skills scout --candidate <own-dir> put every match below the 0.3 fork threshold (own-pack implementation/review skills at 0.28/0.24; docker-k8s-terraform-build-fix, python-build-fix, and nodejs-build-fix all around 0.18-0.19 on generic 'root cause'/'build'/'fail' vocabulary only, none of which cover GitHub Actions/GitLab CI syntax errors, needs:/rules: graph breaks, or GitLab protected-variable scoping) -- an unambiguous create, matching the tool's own raw decision."
25
+ }
26
+ ]
@@ -0,0 +1,41 @@
1
+ {
2
+ "id": "ci-github-gitlab",
3
+ "family": "tool",
4
+ "modules": ["ci-github-gitlab-rules", "ci-github-gitlab-skills"],
5
+ "detectionMarkers": ["github-actions", "gitlab-ci"],
6
+ "provenance": {
7
+ "origin": "authored",
8
+ "sourceRef": "flow 338, Wave 4 batch 6"
9
+ },
10
+ "stability": "experimental",
11
+ "skills": {
12
+ "implement": ["ci-pipeline-implementation"],
13
+ "test": [],
14
+ "review": ["ci-pipeline-code-review"],
15
+ "build-fix": ["ci-pipeline-build-fix"],
16
+ "migrate": []
17
+ },
18
+ "notes": "No separate `test` skill: a CI workflow file has no independent test suite of its own beyond the platform linter (actionlint / GitLab CI Lint), which `ci-pipeline-implementation`'s own Verify step and `ci-pipeline-build-fix` already cover -- a dedicated `test` skill would duplicate one of those two rather than earn a distinct place. No `migrate` skill: migrating GitHub Actions runtime/syntax versions or GitLab CI schema changes did not reach the bar for a distinct skill in this pass (unlike `ts-js-node`/`react`'s ESM/version-upgrade migrations, there is no single recurring migration shape specific enough to write a focused skill and eval suite around yet) -- left for a future batch if a concrete need shows up.",
19
+ "agentProfile": {
20
+ "displayName": "CI (GitHub Actions & GitLab CI)",
21
+ "auditFocus": [
22
+ "a pull_request_target job that checks out github.event.pull_request.head.sha/.ref and then runs a build/test/lint step against it (the pwn-request pattern)",
23
+ "a third-party uses: action pinned to a mutable tag (@v4, @main) instead of a full commit SHA",
24
+ "a workflow-level permissions: write-all, or no permissions: block at all, on a workflow whose jobs do not all need write access",
25
+ "${{ github.event.* }} interpolated directly into a run: shell string instead of passed through env: first, or a GitLab CI script: line that unquotes an untrusted variable, feeds it to eval/sh -c, or reaches it through an unvalidated $[[ inputs.* ]] interpolation",
26
+ "a GitLab CI deploy job consuming a Protected+Masked variable with no rules: restricting it to the protected branch/tag that variable is exposed to",
27
+ "a cache key that never changes, or a job with no timeout-minutes/timeout on a step that can hang"
28
+ ],
29
+ "buildCommands": [
30
+ "actionlint (GitHub Actions workflow files, when configured)",
31
+ "the project's own GitLab CI Lint / gitlab-ci-lint run (.gitlab-ci.yml, when configured)",
32
+ "re-trigger the specific failing run to confirm the named error is resolved"
33
+ ],
34
+ "fixGuardrails": [
35
+ "never grant a broader permissions: scope than the specific failing step needs, and never at the workflow level to fix one job's failure",
36
+ "never unprotect a GitLab CI/CD variable, or remove a rules:/branch gate protecting it, just to make a job on the wrong branch pass",
37
+ "never disable or loosen a rules:/if: condition to make a job \"just run\" without understanding why it was scoped that way",
38
+ "never pin an action to a tag to work around a SHA-pinning lint failure instead of resolving to the correct commit SHA"
39
+ ]
40
+ }
41
+ }
@@ -0,0 +1,77 @@
1
+ ---
2
+ extends: common
3
+ paths: [".github/workflows/*.yml", ".github/workflows/*.yaml", ".github/actions/**/action.yml", ".github/actions/**/action.yaml", ".gitlab-ci.yml", ".gitlab/**/*.yml", ".gitlab/**/*.yaml"]
4
+ metadata:
5
+ origin: authored
6
+ ---
7
+
8
+ # CI workflow patterns (GitHub Actions & GitLab CI)
9
+
10
+ Narrows `core-common-rules`' stack-agnostic design guidance to workflow-YAML
11
+ structure and reuse. Applies only to GitHub Actions workflow files under
12
+ `.github/workflows/`, GitHub composite `action.yml`/`action.yaml` files
13
+ under `.github/actions/`, `.gitlab-ci.yml`, and GitLab CI included config
14
+ under `.gitlab/` — not to Kubernetes manifests,
15
+ Docker Compose files, or any other YAML the `docker-k8s-terraform` pack
16
+ already scopes to.
17
+
18
+ ## Trigger and concurrency shape
19
+
20
+ - Scope `on:`/`rules:` to the events a workflow actually needs (a specific
21
+ branch, path filter, or event type) rather than firing on every push —
22
+ a broad trigger wastes runner minutes and widens the surface a
23
+ misconfigured job can affect.
24
+ - Add a `concurrency:` group (GitHub Actions) or a `resource_group` (GitLab
25
+ CI) to any workflow that deploys or mutates shared state, so a second
26
+ push does not race an in-flight run against the same target — a
27
+ `resource_group` serializes/queues jobs targeting the same resource,
28
+ which is the property a deploy job needs. Do not reach for
29
+ `interruptible: true` here: that flag means the OPPOSITE (this job is
30
+ SAFE to auto-cancel when a newer pipeline starts), which is right for a
31
+ build/test job made obsolete by a newer commit, not for a deploy already
32
+ in flight.
33
+ - Keep `pull_request` as the default trigger for anything that only builds
34
+ or tests untrusted contributions; reach for `pull_request_target` only
35
+ when the job genuinely needs base-repo secrets, and see
36
+ `rules/security.mdc` for what that requires.
37
+
38
+ ## Job structure and reuse
39
+
40
+ - Give every job an explicit `name:` and a `timeout-minutes` (GitHub
41
+ Actions) or job-level `timeout` (GitLab CI) — an unbounded job can hang
42
+ a runner indefinitely on a stuck process or a flaky network call.
43
+ - Factor a step sequence used by more than one workflow into a reusable
44
+ workflow (`workflow_call`) or composite action (GitHub Actions), or an
45
+ `include:`/`extends:` template (GitLab CI), instead of copy-pasting the
46
+ same steps into every workflow file — the copy drifts the moment one
47
+ copy gets a fix the others do not.
48
+ - Pin a `runs-on`/`image:` to a specific, known runner or image tag rather
49
+ than a rolling `latest`, so a runner image update cannot silently change
50
+ build behavior underneath an unrelated change.
51
+ - Use a job-level `needs:` (GitHub Actions) or `needs:`/stage ordering
52
+ (GitLab CI) to express real dependencies between jobs explicitly, rather
53
+ than relying on default stage ordering to imply an ordering the config
54
+ does not actually state.
55
+
56
+ ## Caching and artifacts
57
+
58
+ - Key a cache (`actions/cache`, GitLab's `cache:key`) off the lockfile or
59
+ dependency manifest hash, not a static string — a static key serves a
60
+ stale cache forever and a hash-based key invalidates exactly when the
61
+ dependency set actually changes.
62
+ - Scope `artifacts:`/`actions/upload-artifact` to the files a later job
63
+ actually consumes, with an explicit retention/expiry, rather than
64
+ uploading an entire working directory that balloons storage and
65
+ transfer time.
66
+
67
+ ## Anti-patterns to flag
68
+
69
+ - A workflow that duplicates the same 10+ line step sequence across
70
+ multiple jobs or files instead of a reusable workflow, composite
71
+ action, or `include:`/`extends:` template.
72
+ - A job with no `timeout-minutes`/`timeout` on a step that can hang (a
73
+ network call, a wait loop, an interactive prompt the runner cannot
74
+ answer).
75
+ - A cache key that never changes (a literal string with no hash of the
76
+ lockfile), which either never invalidates or never actually caches
77
+ anything useful.
@@ -0,0 +1,144 @@
1
+ ---
2
+ extends: common
3
+ paths: [".github/workflows/*.yml", ".github/workflows/*.yaml", ".github/actions/**/action.yml", ".github/actions/**/action.yaml", ".gitlab-ci.yml", ".gitlab/**/*.yml", ".gitlab/**/*.yaml"]
4
+ metadata:
5
+ origin: authored
6
+ ---
7
+
8
+ # CI workflow security (GitHub Actions & GitLab CI)
9
+
10
+ Narrows `core-common-rules`' stack-agnostic security rules to the risks
11
+ specific to workflow-YAML configuration. Applies only to GitHub Actions
12
+ workflow files under `.github/workflows/`, GitHub composite `action.yml`/
13
+ `action.yaml` files under `.github/actions/`, `.gitlab-ci.yml`, and GitLab
14
+ CI included config under `.gitlab/`.
15
+
16
+ ## `pull_request_target` and untrusted fork code
17
+
18
+ - `pull_request_target` runs in the context of the **base** repository —
19
+ every repo secret the workflow references is available to it (secrets
20
+ are consumed, not read/write-scoped, so "access" is simply yes/no here),
21
+ and `GITHUB_TOKEN` gets read/write repository permissions by default
22
+ (unless a `permissions:` block explicitly narrows it) — even though the
23
+ triggering pull request can come from an untrusted fork.
24
+ - Never check out and then execute the pull request's own head ref/SHA
25
+ (`actions/checkout` with `ref: ${{ github.event.pull_request.head.sha }}`)
26
+ inside a `pull_request_target` job and then run a build/test/lint
27
+ command against it — that combination (checkout untrusted code + run it
28
+ with base-repo secrets) is the "pwn request" pattern: it hands an
29
+ attacker-controlled pull request everything it needs to exfiltrate
30
+ secrets or push with the workflow's own write access.
31
+ - If a `pull_request_target` job genuinely needs to read fork content
32
+ (e.g. to comment on a PR), check it out read-only and do not execute
33
+ anything from it — no `run:`, no build step, no test step against the
34
+ fork's own code — or route the job through a privilege-separated
35
+ `workflow_run` split (a build job with no secrets, a separate
36
+ privileged job that only consumes safe artifacts) instead.
37
+ - When a job only needs to build/test a contribution and does not need
38
+ base-repo secrets or write access, use `pull_request`, not
39
+ `pull_request_target` — when a `pull_request` run is triggered from a
40
+ fork, `GITHUB_TOKEN` (still the base repo's own token, not a separate
41
+ "fork token") gets read-only permissions, and no other repo secret is
42
+ passed to the runner at all.
43
+
44
+ ## Pin third-party actions to a commit SHA
45
+
46
+ - Reference a third-party action by its full-length commit SHA
47
+ (`uses: actions/checkout@<40-char-sha>`), not a mutable tag like `@v4`
48
+ or a branch name — a tag can be moved by the action's own maintainer
49
+ (or, if the account is compromised, by an attacker) to point at a
50
+ different, malicious commit without the reference in your workflow ever
51
+ changing.
52
+ - A first-party action from `actions/*` maintained by GitHub itself is
53
+ lower risk than an arbitrary third-party action, but the SHA-pinning
54
+ rule still applies to any action outside the repository's own
55
+ `.github/actions/` — do not special-case a well-known org as exempt.
56
+ - When a comment noting the tag the SHA corresponds to helps readability
57
+ (`uses: actions/checkout@<sha> # v4.2.0`), that comment is documentation
58
+ only — it is the SHA, not the comment, that pins the actual code that
59
+ runs.
60
+
61
+ ## Least-privilege `permissions:`
62
+
63
+ - Default to `permissions: {}` (or `permissions: read-all` only when read
64
+ access is genuinely needed everywhere) at the workflow level, then grant
65
+ the specific scopes a job needs (`contents: write`, `pull-requests:
66
+ write`, `id-token: write` for OIDC, etc.) at the **job** level, not the
67
+ workflow level — a workflow-level `permissions: write-all` (or no
68
+ `permissions:` key at all, which can default to broad access depending
69
+ on the repository/org setting) hands every job in the file whatever
70
+ scope the most-privileged job needed, even ones that only read.
71
+ - Grant `id-token: write` only to the job that actually performs OIDC
72
+ federation (cloud credential exchange); never at the workflow level
73
+ alongside jobs that do not need it.
74
+
75
+ ## Script injection via untrusted expression expansion
76
+
77
+ - Never interpolate an untrusted `${{ github.event.* }}` value (a PR
78
+ title, issue body, commit message, branch name, or any other
79
+ attacker-influenced field) directly into a `run:` shell string — the
80
+ expression is substituted into the generated script **before** the
81
+ shell runs, so a value containing shell metacharacters (`"`, `` ` ``,
82
+ `$(...)`) can break out of the intended string and execute arbitrary
83
+ commands on the runner with whatever permissions that job has.
84
+ - Pass the untrusted value through an intermediate `env:` variable first,
85
+ then reference the environment variable (`"$TITLE"`, quoted) inside the
86
+ script, instead of interpolating `${{ github.event.pull_request.title
87
+ }}` directly into the `run:` string — the environment variable is set
88
+ in memory and never re-enters shell-script generation, which is what
89
+ actually neutralizes the injection.
90
+ - GitLab CI's own script-injection risk is NOT the same mechanism, and the
91
+ `env:`-indirection fix above does not carry over: every GitLab CI/CD
92
+ variable (a predefined one like `CI_MERGE_REQUEST_TITLE`, or one you
93
+ declare under `variables:`) is already an ordinary shell environment
94
+ variable by the time `script:` runs — there is no separate "before the
95
+ shell sees it" substitution step to intercept the way `${{ }}` needs
96
+ routing through `env:` first. Wrapping an already-a-variable in another
97
+ `variables:` entry changes nothing about how the shell expands it.
98
+ The real risks, and their real fixes:
99
+ - **Unquoted expansion**: `script: - rm -rf $CI_MERGE_REQUEST_TITLE` lets
100
+ the shell word-split and glob-expand an untrusted value; always quote
101
+ (`"$CI_MERGE_REQUEST_TITLE"`), and never build a command string by
102
+ concatenating an untrusted variable into one for `eval`/`sh -c`
103
+ (`eval "some-command $CI_MERGE_REQUEST_TITLE"` is exploitable however
104
+ the variable got its value).
105
+ - **Config-time interpolation via CI/CD inputs** (`$[[ inputs.x ]]`,
106
+ used in a component/included config's `rules:`/`script:`): this IS
107
+ substituted into the YAML/expression text before the job is even
108
+ created, the same class of risk `${{ }}` is in GitHub Actions — do not
109
+ feed an untrusted pipeline or trigger input value (a `spec:inputs`
110
+ value the pipeline/component was invoked with, not a predefined
111
+ variable like an MR title) into an `inputs:`-driven `$[[ ]]`
112
+ interpolation that reaches a `script:` line or a `rules:if:`
113
+ expression without validating/escaping it first.
114
+
115
+ ## GitLab CI protected variables and branches
116
+
117
+ - A CI/CD variable marked "Protected" is only exposed to pipelines running
118
+ on a protected branch or a protected tag — an unprotected branch's
119
+ pipeline (including one from a fork merge request, when forks are
120
+ allowed) never receives it. Do not rely on a protected variable being
121
+ present in a job whose `rules:`/`only:`/`except:` allow it to run on a
122
+ non-protected ref; the value will simply be empty there, not an error.
123
+ - Mark any variable carrying a credential, deploy token, or signing
124
+ secret both "Protected" and "Masked" and restrict the job that consumes
125
+ it to protected branches/tags via `rules:` — a deploy job reachable from
126
+ an unprotected branch or an open merge request should not have access
127
+ to production credentials at all.
128
+
129
+ ## Anti-patterns to flag
130
+
131
+ - A `pull_request_target` job that checks out `github.event.pull_request.head.sha`
132
+ (or `.ref`) and then runs a build/test/lint step against that checkout.
133
+ - Any `uses:` line pinned to a tag (`@v4`, `@main`) instead of a full
134
+ commit SHA for a third-party action.
135
+ - A workflow-level `permissions: write-all`, or no `permissions:` block at
136
+ all, on a workflow with jobs that do not all need write access.
137
+ - `${{ github.event.* }}` interpolated directly inside a `run:` string
138
+ instead of passed through `env:` first.
139
+ - A GitLab CI `script:` unquoted use of a variable holding untrusted input
140
+ (an MR title, a branch name), or that variable concatenated into a
141
+ string handed to `eval`/`sh -c`.
142
+ - An untrusted value reaching a GitLab CI/CD `inputs:`-driven `$[[ ]]`
143
+ interpolation in a `script:` line or a `rules:if:` expression with no
144
+ validation.
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: ci-pipeline-build-fix
3
+ description: "Use when a GitHub Actions workflow run or GitLab CI pipeline is failing at the pipeline-configuration level -- YAML syntax errors, an invalid trigger/job key, a missing or misscoped permissions block breaking a step, a job failing because a protected variable is unavailable on its branch, or a broken needs:/rules:/dependency graph -- with the smallest root-cause fix to the config itself. Not for a failure in the application code the pipeline runs (a Python, TypeScript, Go, or other language compile/import/test error -- fix the code, or use that language's own build-fix skill), not for a Dockerfile/Kubernetes/Terraform build or validation failure (use docker-k8s-terraform-build-fix), and not for authoring a new workflow/pipeline from scratch (use ci-pipeline-implementation)."
4
+ triggers:
5
+ - "this GitHub Actions workflow fails to parse, what's wrong with the YAML"
6
+ - "our deploy step lost access to a GitLab variable it needs"
7
+ - "this job says permission denied writing to the PR, fix the workflow"
8
+ - "the pipeline says invalid needs: reference, fix the job graph"
9
+ - "GitLab CI says this job's rules: never match, why doesn't it run"
10
+ - "actions/checkout is failing with an unrecognized input, fix the workflow"
11
+ - "this step references a repo secret that doesn't exist, the job just fails silently"
12
+ metadata:
13
+ origin: authored
14
+ category: build-fix
15
+ version: "1.0.0"
16
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
17
+ license: "MIT"
18
+ ---
19
+
20
+ # CI pipeline build-fix (GitHub Actions & GitLab CI)
21
+
22
+ Fix a GitHub Actions workflow or GitLab CI pipeline that is failing at the
23
+ **pipeline-configuration level**: YAML syntax, an invalid or misspelled
24
+ trigger/job key, a `permissions:` scope too narrow for what a step needs,
25
+ a job that cannot see a variable because of GitLab's protected-branch
26
+ scoping, or a broken `needs:`/`rules:`/dependency graph. Scoped to the
27
+ workflow file itself — a failure in the application code the pipeline
28
+ builds or tests belongs to that stack's own `build-fix`/`test` skill, not
29
+ here; do not widen a permissions grant or disable a `rules:` gate just to
30
+ make a run go green without understanding why it was scoped that way.
31
+
32
+ ## Workflow
33
+
34
+ ### Step 1: Read the actual failure, not just the job name
35
+
36
+ 1. Get the exact error from the run log or the platform's own linter
37
+ (GitHub Actions' "Invalid workflow file" annotation, GitLab's CI Lint
38
+ page/`gitlab-ci-lint` output) — a config-level failure usually names
39
+ the exact line and key it choked on.
40
+ 2. Classify the failure: YAML syntax (indentation, an unquoted string
41
+ colliding with a reserved word), an unknown/misspelled key, a
42
+ `permissions:` scope missing what a step needs (e.g. `Resource not
43
+ accessible by integration` writing a PR comment), a variable that is
44
+ `Protected` but the job runs on an unprotected branch, or a `needs:`/
45
+ `rules:`/dependency reference to a job name that does not exist or
46
+ never runs on this ref.
47
+
48
+ ### Step 2: Fix the smallest root cause
49
+
50
+ - **YAML syntax**: fix the actual indentation/quoting/key at the named
51
+ line; do not restructure unrelated parts of the file.
52
+ - **Missing permission**: grant the specific scope the failing step
53
+ actually needs at the job level (not a blanket `write-all` at the
54
+ workflow level) — trace the failing API call back to the
55
+ [documented scope it requires](https://docs.github.com/en/actions/reference/authentication-in-a-workflow)
56
+ rather than guessing.
57
+ - **Protected variable unavailable**: either move the job's `rules:` so
58
+ it only runs on the protected branch/tag the variable is scoped to, or
59
+ — if the job genuinely must run elsewhere — use a separate,
60
+ non-protected variable there and keep the protected one for the
61
+ protected-branch job only. Never unprotect the variable just to make an
62
+ unprotected-branch job pass.
63
+ - **Broken `needs:`/`rules:`/dependency graph**: fix the job-name
64
+ reference or the ordering itself; confirm the referenced job actually
65
+ exists and actually runs on the ref this job runs on (a `needs:`
66
+ reference to a job whose own `rules:`/`if:` skips it on this ref is a
67
+ common cause of "job never starts").
68
+
69
+ ### Step 3: Verify
70
+
71
+ - Re-run the platform's own linter (`actionlint`, GitLab's CI Lint page,
72
+ or a local `gitlab-ci-lint` run) against the fixed file.
73
+ - Confirm the fix does not widen `permissions:` past what the failing
74
+ step specifically needs, and does not remove a `rules:`/protected-branch
75
+ gate instead of routing the job to the right ref.
76
+ - Trigger (or describe how to trigger) the same failing run once more to
77
+ confirm the specific error is gone.
78
+
79
+ ### Step 4: Report
80
+
81
+ ```
82
+ Fixed: .github/workflows/deploy.yml
83
+ - permissions.pull-requests was missing "write"; the step posting a PR
84
+ comment needed it. Added at job level only (not workflow level).
85
+ - actionlint passes; re-run confirms the comment step now succeeds.
86
+ ```
87
+
88
+ ## Rules
89
+
90
+ - Fix the smallest root cause named by the actual linter/run-log error;
91
+ do not restructure unrelated jobs or steps while fixing one failure.
92
+ - Never grant a broader `permissions:` scope than the specific failing
93
+ step needs, and never grant it at the workflow level to fix a
94
+ single job's failure.
95
+ - Never unprotect a GitLab CI/CD variable, or remove a `rules:`/branch
96
+ gate protecting it, just to make a job on the wrong branch pass —
97
+ route the job to the right branch/ref, or use a separate variable
98
+ there.
99
+ - Never disable or loosen a `rules:`/`if:` condition to make a job "just
100
+ run" without first understanding why it was scoped the way it was.
101
+
102
+ ## Red Flags
103
+
104
+ | Rationalization | Why it is wrong |
105
+ |---|---|
106
+ | "I'll set permissions: write-all, it's faster than figuring out which scope this needs" | That fixes this failure by silently widening every other job in the file too; the actual fix (one scope, one job) costs one extra line and leaves the rest of the file at its intended privilege |
107
+ | "This variable is protected and the job can't see it, I'll just uncheck Protected" | That exposes the credential to every unprotected branch's pipeline, including forks if allowed -- move the job to the protected ref instead, or use a separate non-protected variable for the non-protected case |
108
+ | "The rules: condition is why this job won't run, I'll just delete it" | Deleting the gate makes the job run everywhere the rule was written to exclude, which is usually the actual cause of the failure being investigated in the first place, not the fix for it |
109
+
110
+ ## Verification
111
+
112
+ Do not report the fix done until all of the following hold:
113
+
114
+ - The platform's own linter (actionlint / GitLab CI Lint) passes against
115
+ the fixed file.
116
+ - The specific error named in the original failure is resolved, not
117
+ worked around by widening scope or disabling a gate.
118
+ - No `permissions:` grant was added beyond what the failing step
119
+ specifically needs.
120
+ - No `rules:`/protected-variable gate was removed or loosened as part of
121
+ the fix.
@@ -0,0 +1,73 @@
1
+ {
2
+ "triggers": {
3
+ "positive": [
4
+ "This workflow won't even parse -- GitHub says there's a YAML syntax error",
5
+ "Our deploy job can't read the API_KEY variable even though it's set in GitLab",
6
+ "The bot says Resource not accessible by integration when posting a PR comment",
7
+ "GitLab keeps saying this job's rules never match so it just gets skipped",
8
+ "actions/checkout is erroring out with an unrecognized input on this job",
9
+ "This job references another job in needs: that GitHub says doesn't exist"
10
+ ],
11
+ "negative": [
12
+ "The build fails because a Python import is missing, fix the code",
13
+ "Our TypeScript compile step is failing with a type error, fix it",
14
+ "The Go binary fails go vet, please fix the source",
15
+ "This Dockerfile fails to build because of a missing apt package",
16
+ "Write a new workflow that lints our code on every push",
17
+ "Review this workflow for security issues before we merge it"
18
+ ]
19
+ },
20
+ "scenarios": [
21
+ {
22
+ "id": "permission-scope-fix",
23
+ "prompt": "This job fails with 'Resource not accessible by integration' when it tries to comment on the pull request. The workflow currently has no permissions: block. How do I fix it?",
24
+ "strictness": "high",
25
+ "expected_behavior": [
26
+ {
27
+ "grader": "judge",
28
+ "rubric": "A correct answer identifies the missing pull-requests: write scope as the root cause of the 'Resource not accessible by integration' failure, fixes it by adding that specific scope (plus contents: read if the job checks out code) scoped no wider than the failing job needs -- at the job level when the workflow has other jobs that should not get it, or at the workflow level only if this is the workflow's only job -- and confirms the fix does not widen access beyond what the failing comment step needs (no workflow-level write-all, no unscoped grant reaching jobs that do not need it).",
29
+ "pass_criteria": [
30
+ "Identifies the missing permissions: pull-requests: write scope as the root cause of the 'Resource not accessible by integration' failure.",
31
+ "Scopes the grant to only what this job needs (pull-requests: write, plus contents: read if the job also checks out code) rather than a broad or unscoped grant -- placing it at the job level is the clearly correct choice when the workflow has other jobs that do not need it, and is an acceptable choice (not a requirement to fail on) when this is the workflow's only job.",
32
+ "States or confirms that the fix does not widen scope beyond what the failing comment step specifically needs."
33
+ ],
34
+ "fail_criteria": [
35
+ "Recommends permissions: write-all (or another broad, unscoped grant, at either the workflow or job level) as the fix instead of the specific pull-requests: write (+ contents: read if needed) scope. Mentioning write-all only to warn against it is not a failure."
36
+ ]
37
+ }
38
+ ],
39
+ "calibration": {
40
+ "known_right": "The root cause is that the workflow has no permissions: block at all, so the default GITHUB_TOKEN scope doesn't include write access to pull requests, and the comment step fails with 'Resource not accessible by integration'. Fix: add `permissions: { contents: read, pull-requests: write }` directly on this job (not at the workflow level, so the rest of the file stays at its current, narrower scope) -- contents: read is included because setting any explicit permissions: block resets every unlisted scope to none, and this job still needs to check out the repo. That grants exactly the access the comment step needs and nothing more. Re-run the job after adding it to confirm the comment now posts successfully.",
41
+ "known_wrong": "Simplest fix here is to add `permissions: write-all` at the top of the workflow -- that guarantees this job (and anything else added later) has whatever access it needs without having to track down the exact scope each time.",
42
+ "vague": "You'll need to add the right permissions so the comment step can post.",
43
+ "subtle_wrong": "Add `permissions: { contents: write, pull-requests: write, issues: write }` at the workflow level -- that covers the comment step and leaves room for other jobs that might need similar write access later, so nobody has to keep revisiting the permissions block every time a new job needs something."
44
+ },
45
+ "anti_patterns": ["write-all"]
46
+ },
47
+ {
48
+ "id": "protected-variable-fix",
49
+ "prompt": "The deploy job can't see DEPLOY_TOKEN even though it's defined in GitLab CI/CD variables and marked Protected. The pipeline runs on a feature branch, which is not a protected branch. How do I fix this?",
50
+ "strictness": "high",
51
+ "expected_behavior": [
52
+ {
53
+ "grader": "judge",
54
+ "rubric": "A correct answer identifies that DEPLOY_TOKEN being unavailable is because it is marked Protected and the pipeline is running on an unprotected feature branch, and fixes it by restricting the deploy job's rules: to the protected branch/tag (or by introducing a separate non-protected variable for the feature-branch case) rather than by unprotecting DEPLOY_TOKEN.",
55
+ "pass_criteria": [
56
+ "Identifies that DEPLOY_TOKEN is unavailable because it is a Protected variable and the pipeline is running on an unprotected feature branch, not because the variable is misconfigured or missing.",
57
+ "Recommends restricting the deploy job (via rules:) to run only on the protected branch/tag DEPLOY_TOKEN is exposed to, or introducing a separate non-protected variable for the feature-branch case.",
58
+ "Does not recommend removing Protected status from DEPLOY_TOKEN as part of the fix."
59
+ ],
60
+ "fail_criteria": [
61
+ "Recommends unprotecting or removing Protected status from DEPLOY_TOKEN so it becomes available on the feature branch. Mentioning that this is what not to do is not a failure."
62
+ ]
63
+ }
64
+ ],
65
+ "calibration": {
66
+ "known_right": "DEPLOY_TOKEN is marked Protected in GitLab, which means it's only exposed to pipelines running on a protected branch or tag -- a feature branch pipeline simply never receives it, which is why it reads as empty/missing rather than erroring loudly. Fix: restrict this deploy job's rules: so it only runs on the protected branch (or tag) that DEPLOY_TOKEN is actually scoped to, rather than on every feature branch. If the deploy job genuinely needs to run from feature branches too (e.g. for a staging deploy), use a separate, non-protected variable for that case instead of exposing the production DEPLOY_TOKEN there.",
67
+ "known_wrong": "Easiest fix is to open the variable settings in GitLab and uncheck 'Protected' on DEPLOY_TOKEN -- then it'll be available to every branch's pipeline, including this feature branch, with no other changes needed.",
68
+ "vague": "You probably just need to adjust the variable settings so the deploy job can see it.",
69
+ "subtle_wrong": "Since the feature branch needs the token right now for testing, go ahead and unprotect DEPLOY_TOKEN temporarily -- you can always re-protect it later once the feature branch work is done and merged."
70
+ }
71
+ }
72
+ ]
73
+ }
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: ci-pipeline-code-review
3
+ description: "Use when reviewing a GitHub Actions workflow (.github/workflows/*.yml) or GitLab CI pipeline (.gitlab-ci.yml) change for security and structural risk -- pull_request_target combined with untrusted checkout, tag-pinned third-party actions, missing least-privilege permissions, script injection via unsanitized event/variable interpolation, and unprotected access to deploy secrets. Read-only, no edits."
4
+ triggers:
5
+ - "review this GitHub Actions workflow diff for security issues"
6
+ - "check this .gitlab-ci.yml change for exposed secrets"
7
+ - "does this workflow have a script injection risk?"
8
+ - "review this pull_request_target job for pwn request risk"
9
+ - "check whether these third-party actions are pinned safely"
10
+ - "review this deploy job for least-privilege permissions"
11
+ metadata:
12
+ origin: authored
13
+ category: review
14
+ version: "1.0.0"
15
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
16
+ license: "MIT"
17
+ ---
18
+
19
+ # CI pipeline security review (GitHub Actions & GitLab CI)
20
+
21
+ Review a GitHub Actions workflow or GitLab CI pipeline change for the
22
+ specific risks `rules/security.mdc` catalogs: `pull_request_target`
23
+ combined with untrusted checkout, mutable-tag action pinning, missing
24
+ least-privilege `permissions:`, script injection via unsanitized
25
+ `${{ github.event.* }}`/CI-variable interpolation, and unprotected access
26
+ to deploy secrets. **This skill is strictly read-only** — it reports
27
+ findings and a fix direction, and never edits, patches, or claims to have
28
+ applied even a partial or proof-of-concept change to the workflow file
29
+ under review; a change belongs to `ci-pipeline-implementation`, not here.
30
+
31
+ ## Workflow
32
+
33
+ ### Step 1: Scope the diff
34
+
35
+ 1. Read the full diff of every changed `.github/workflows/*.yml` or
36
+ `.gitlab-ci.yml` file, not just the added lines — a security-relevant
37
+ line (a `permissions:` block, a `uses:` pin) removed or weakened is as
38
+ much a finding as one added.
39
+ 2. Note the trigger(s) each changed/added job runs under
40
+ (`pull_request`, `pull_request_target`, `push`, `workflow_dispatch`,
41
+ GitLab's `rules:`/pipeline source) — the trigger determines which
42
+ findings below even apply.
43
+
44
+ ### Step 2: Check each risk category
45
+
46
+ - **`pull_request_target` + untrusted checkout.** Does any
47
+ `pull_request_target`-triggered job check out the contributor's own fork
48
+ ref or head commit SHA (via the pull request event's own `head` field)
49
+ and then run a build/test/lint step against that checkout?
50
+ That combination is the "pwn request" pattern — flag it regardless of
51
+ how innocuous the executed step looks, since the risk is the
52
+ combination, not any one line.
53
+ - **Action pinning.** Does every third-party `uses:` line (anything
54
+ outside the repository's own `.github/actions/`) reference a full
55
+ commit SHA rather than a tag (`@v4`) or branch name? A tag comment next
56
+ to a SHA is fine; a bare tag as the actual pin is not.
57
+ - **`permissions:` scope.** Is there a workflow-level `permissions:
58
+ write-all`, or no `permissions:` key at all on a workflow whose jobs do
59
+ not all need write access? Is `id-token: write` granted only to the job
60
+ that actually performs OIDC federation?
61
+ - **Script injection.** For GitHub Actions: does any `run:` step
62
+ interpolate `${{ github.event.* }}` (a PR title, issue body, branch
63
+ name, commit message) directly into the shell string, instead of
64
+ routing it through an intermediate `env:` entry first? For GitLab CI:
65
+ does a `script:` line use an untrusted variable (an MR title, a branch
66
+ name) unquoted, or concatenate it into a string handed to `eval`/
67
+ `sh -c` — routing it through another `variables:` entry does not fix
68
+ this, since every GitLab CI/CD variable is already a shell environment
69
+ variable by the time `script:` runs; also check any `$[[ inputs.* ]]`
70
+ interpolation for untrusted input, since that IS substituted before the
71
+ job is created.
72
+ - **GitLab protected variables/branches.** Does a deploy job that
73
+ consumes a credential-bearing variable have `rules:` restricting it to
74
+ the protected branch/tag that variable is actually exposed to, and is
75
+ that variable itself marked Protected and Masked?
76
+
77
+ ### Step 3: Report findings
78
+
79
+ For each finding: name the exact line/job, state which risk category it
80
+ falls under, explain the concrete consequence (not just "this is
81
+ insecure"), and give a fix direction from `rules/security.mdc` — never
82
+ apply the fix yourself.
83
+
84
+ ```
85
+ Findings in .github/workflows/pr-review.yml:
86
+ - Job "test" (line 12): triggered by pull_request_target, this job
87
+ checks out the fork's own head SHA and then runs `make test` against
88
+ it -- pwn-request pattern. Fix direction: switch to `pull_request`, or split
89
+ into an unprivileged build job + a separate privileged job that only
90
+ consumes safe artifacts.
91
+ - Job "test" (line 18): actions/checkout@v4 -- pinned to a mutable tag.
92
+ Fix direction: pin to actions/checkout's full commit SHA.
93
+ No other findings.
94
+ ```
95
+
96
+ ## Rules
97
+
98
+ - Read-only: report findings and fix direction, never edit the workflow
99
+ file or claim a fix was applied, even partially or as a proof of
100
+ concept.
101
+ - Flag `pull_request_target` + untrusted-checkout-and-execute as a
102
+ finding every time it appears, regardless of how trivial the executed
103
+ step looks.
104
+ - Flag any third-party action pinned to a tag rather than a full commit
105
+ SHA.
106
+ - Flag a workflow-level `permissions: write-all` (or no `permissions:` at
107
+ all) alongside jobs that do not all need write access.
108
+ - Flag `${{ github.event.* }}` interpolated directly into a `run:` string
109
+ instead of passed through `env:` first (GitHub Actions). Flag a GitLab
110
+ CI `script:` line that uses an untrusted variable unquoted or
111
+ concatenates it into `eval`/`sh -c` (routing it through another
112
+ `variables:` entry does not fix this), or an untrusted pipeline/trigger
113
+ input reaching a `$[[ inputs.* ]]` interpolation unvalidated.
114
+
115
+ ## Red Flags
116
+
117
+ | Rationalization | Why it is wrong |
118
+ |---|---|
119
+ | "This is just a docs/CI change, security review doesn't apply" | A CI workflow change is exactly the surface this skill exists for -- a workflow file itself grants access to secrets and write scope, independent of what other code changed in the same PR |
120
+ | "The tag is from the official actions/ org, it's fine to leave unpinned" | The pinning risk is about the tag being movable, not about who currently controls it -- a first-party org account can still be compromised, and the fix costs nothing |
121
+ | "I noticed the fix while reviewing so I went ahead and applied it" | This skill is read-only; report the finding and fix direction, and let the implementation skill or the author apply it |
122
+
123
+ ## Verification
124
+
125
+ Do not report the review done until all of the following hold:
126
+
127
+ - Every changed job's trigger was checked against the
128
+ `pull_request_target` + untrusted-checkout pattern.
129
+ - Every third-party `uses:` line in the diff was checked for tag vs.
130
+ commit-SHA pinning.
131
+ - The `permissions:` block (workflow- and job-level) was checked for
132
+ least privilege.
133
+ - Every `run:` step touching `${{ github.event.* }}` was checked for
134
+ direct interpolation vs. intermediate `env:` (GitHub Actions), and every
135
+ GitLab CI `script:` step touching an untrusted variable was checked for
136
+ unquoted use, `eval`/`sh -c` concatenation, or an unvalidated
137
+ `$[[ inputs.* ]]` interpolation.
138
+ - No finding was silently skipped because the surrounding change looked
139
+ unrelated to security.