Repository navigation
Description
Scope
These ideas come from the issues, PRs and discussions of other projects:
- rhysd/actionlint and the fork kjanat/actionlint
- zizmor (
mainafter 1.30.1) - ghalint, ghlint, pinact
- poutine, CodeQL actions queries, octoscan, sisakulint, Scorecard, Semgrep, gato-x
- GitHub's changelog and the workflow schema (
workflow-v1.0.json)
Ideas that jactionlint already covers were dropped. Items marked repro were reproduced with a build of c7254590; the others are not measured. Each item should get its own issue or PR when someone picks it up. This issue only tracks them. It is separate from #121 and #107: items already listed there are not repeated here.
1. Bugs in jactionlint
- The
Generateworkflow has failed on every run since the rename (runs).go run ./scripts/generate-popular-actions -dexits 1 when it finds new major versions (actions/setup-java@v6,dawidd6/action-download-artifact@v22, …). As a resultgo generatenever runs, and the popular-action, webhook and availability data stays frozen, even within known majors. Also check the "Not found" entries in the log (release-drafter@v8,claude-code-action@v2, …). (ci(generate): keep detecting new action releases from failing the data refresh #169, chore(generate): refresh the generated data and keep merge_group destroyed #172) - The
-latestrunner labels map to old images (repro). Inrule_runner_label.go,windows-latestmaps to 2022 andmacos-latestto macOS 15. (fix(runner-label): follow the images behind the -latest labels #171)- False conflicts:
[windows-latest, windows-2025],[macos-latest, macos-26]and[windows-2025, windows-2025-vs2026]. - A missed conflict:
[windows-latest, windows-2022]. mutable-runner-labelsuggests the older image.ubuntu-latestmoves to 26.04 between 2026-10-19 and 2026-11-19 (runner-images#14748, #14167).
- False conflicts:
- The input checks for popular actions skip SHA pins and full semver refs (repro).
actions/cache@<sha> # v4.2.4andactions/cache@v4.2.4withkeys:give no finding, while@v4is reported. The default profile requires SHA pins, so this silently turns offmissing-action-input,unknown-action-inputanddeprecated-action-input.outdated-action-runnerhas the same gap (actionlint#622). Fix: take the major version from the version comment or the semver tag. (fix(action): check popular actions pinned to a full version or to a commit with a version comment #174) - Whole-value expressions are rejected (repro). The runner schema gives these keys a
contextlist, so an expression is valid there: (fix(parser): accept a whole mapping or list written as one expression where the schema allows it #178)with: ${{ fromJSON(vars.W) }}anddefaults: run: ${{ … }}are reported asworkflow-syntaxerrors in thecorrectnessprofile (zizmor#1769).strategy: ${{ fromJSON('…') }}gets a "scalar node" error and thenundefined-propertyonmatrix.*.
- False positives from schema drift (repro): (fix(syntax): accept keys and values the workflow schema allows but the docs do not list #170)
- top-level
description:(languageservices@50b08a3) - the
stackedactivity type ofpull_requestandpull_request_target(languageservices#384) - the
drivespermission scope (languageservices#380) github.artifactsandgithub.artifacts_list- empty strings in choice
options snapshot.ifwithsteps,env,job,runner, the status functions andhashFiles(runner#4443)image_version.typescancel-timeout-minutes${{ insert }}as a mapping key (actionlint#349)
- top-level
- False negatives from schema drift (repro): (fix(syntax): report what the runner rejects in schedules, merge_group, self-repository uses and action.yml #186, chore(generate): refresh the generated data and keep merge_group destroyed #172) Not done:
github.job_workflow_sha, where the sources disagree on whether it is populated.- a
scheduleentry withoutcron merge_group: types: [destroyed]$/references with a missing directory, an@refsuffix or../(languageservices#381)timeout-minuteson a composite step (actionlint#277)${{ env.X }}in thedefaultof an action inputpre-ifandpost-if, which are never checked (languageservices#317)github.job_workflow_sha, which is accepted but is always empty (languageservices#347)
- a
- A conformance test against the runner's
workflow-v1.0.jsonandaction_yaml.json, at a pinned commit that is bumped weekly. It would compare the keys,allowed-values,contextlists and whole-value expressions with the parser tables andavailability.go. It would have caught most of the two items above; the webhook generator scrapes the docs, which lag the schema. (test(schema): compare the parser with the schemas of the runner at a pinned commit #198) - The hooks in
.pre-commit-hooks.yamlusefiles: ^\.github/workflows/, so editing anaction.ymlordependabot.yml, which jactionlint lints, does not run them. (fix(pre-commit): run the hooks for action.yml and dependabot.yml, add a bundled-shellcheck hook #181) -
untrusted-checkout: (fix(untrusted-checkout): follow pull/N/head fetches, skip merged pull requests, count sed -f, awk -f, tar -P and unzip -: #175)- It misses
git fetch origin pull/N/head:prwritten withoutrefs/(repro). - It reports
pull_request_target: closedwithif: github.event.pull_request.merged == true(repro).
- It misses
-
missing-timeoutreports a job whose steps all settimeout-minutes(repro; the exception of ghalint 012). (fix(missing-timeout): add accept-step-timeouts for jobs whose steps all set timeout-minutes #252) - Small issues (repro): (feat(external-tools): say when shellcheck or pyflakes did not run #188) All three are fixed on
main(checked 2026-10-13).excessive-permissionsprints an empty description formodels: write.- A 64-hex (SHA-256) commit pin is reported as unpinned (runner#4347).
--shellcheck=/missing/pathis ignored without a word.
2. Platform changes with dates
-
The fork-PR protection of
actions/checkout(changelog, enforced since 2026-07-20 on every floating major except v1).untrusted-checkoutgives the same message with and withoutallow-unsafe-pr-checkout: true(repro). It should: (feat(untrusted-checkout): honor the fork pull request protection of actions/checkout #183, feat(untrusted-checkout): honor the fork pull request protection of actions/checkout #203)- report the opt-out on its own line;
- say whether the checkout is blocked (a floating major), exploitable (the opt-out, or a SHA pin below v2.8.0, v3.7.0, v4.4.0, v5.1.0 or v6.1.0) or not covered (
gitorgh).
Done in poutine#450; in progress in zizmor#2134.
-
.github/workflows/actions.lock(technical preview, enforced on GHEC; zizmor#2371, codeql#22155). (feat(unpinned-uses): accept a ref which the Actions lock file pins to a commit #228, feat(actions-lock): lint GitHub's Actions lock file against the workflows #237)unpinned-usesshould accept the refs that the lock pins.- The pin fix must not inline a SHA there.
- Report missing and stale entries.
- With
--online, check the locked commits.
-
Node 20 was removed on 2026-09-23 (changelog). Warn on: (feat(deprecated-node-runtime): report a Node.js runtime that GitHub removed in the metadata of an action (pedantic) #225, feat(deprecated-node-runtime): report popular actions at majors that declare node20 (pedantic) #230, feat(deprecated-node-runtime): report the environment variables that chose the Node.js runtime (pedantic) #234)
runs.using: node20;- popular node20 majors (
checkout@v4,codeql-action@v3, …); ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSIONandFORCE_JAVASCRIPT_ACTIONS_TO_NODE24, which no longer do anything.
-
Runner labels being retired: (feat(runner-label): warn ahead of the retirement of a runner label and name it afterwards #182)
- Warn ahead of time for
macos-14*(retires 2026-11-02, changelog) andubuntu-22.04*(2027-04-17). - For a label already retired, name its replacement instead of listing every label.
- Warn ahead of time for
-
ubuntu-slimcaps jobs at 15 minutes and has no Docker daemon. (feat(ubuntu-slim): report what the single-CPU runner cannot run #190)- Report a higher
timeout-minutes,container:,services:anddocker://steps. - Cap the missing-timeout fix at 15 on that label.
- Report a higher
-
pull_request_targetis disabled by default in public repositories from 2026-11-02 (changelog). Mention it in thedangerous-triggersmessage and the docs. (feat(dangerous-triggers): say what the token and the secrets can do, and make a workflow without privileges pedantic #189) -
Check whether GitHub now trims the trailing newline of a block
if:(changelog 2026-01-29). If it does,if-always-trueis a false positive forif: |(not confirmed). (fix(if-always-true): do not report the newlines at the end of a block scalar #226)
3. Security detection gaps
-
untrusted-checkoutonissue_comment(repro):/testbots that rungh pr checkout Nor check outrefs/pull/N/head, then build. Accept the gates of CodeQL's ControlChecks. poutine, CodeQL, octoscan and gato-x all cover this. (feat(untrusted-checkout): cover issue_comment workflows that build a pull request #193, feat(untrusted-checkout): cover issue_comment workflows that build a pull request #206) -
untrusted-checkoutthrough local reusable workflows and composite actions (repro):with: ref: ${{ github.event.pull_request.head.sha }}→ref: ${{ inputs.ref }}. (feat(untrusted-checkout): follow the head of a pull request into local reusable workflows and composite actions #208) - Actions that run workspace code: the list in
untrusted_helpers.gohas 4 entries (repro). Add the actions of poutine and CodeQL: setup-ruby withbundler-cache, build-push-action, goreleaser, megalinter, super-linter, checkov, sonar, cibuildwheel, maturin, cypress, … Or generate the list from LOTP (zizmor#1948). The same check should also catchtar -P,sed -f,awk -fandunzip -:. (feat(untrusted-checkout): count the actions that run the workspace, with the input that makes them do it #213) - Command injection through env vars (repro):
eval "$BODY",bash -c "…$BODY", a variable in command position, andexecSync(process.env.X)in github-script (CodeQL CWE-078). (feat(command-injection): report input that a script runs as code after it moved to env #202) - TOCTOU (repro): an
environment:or label gate combined with a mutablehead.ref,refs/pull/N/mergeorgh pr checkout, or a label gate whosetypes:includesynchronize(CodeQL CWE-367). (feat(untrusted-checkout): report gates that hold at one moment in front of code that can change, and labels that last #220) -
template-injection: free text written by outsiders is reported only by the pedantic tier (repro):changes.title.from,changes.body.from,head.repo.homepage,pages[*].title,workflow_run.pathandmerge_group.head_commit.message(CodeQL list). (feat(template-injection): report more free text of outsiders in the default profile #173) -
dangerous-triggers: takepermissions: {}, read-only scopes and the absence of secrets into account (zizmor#398, 11 👍). (feat(dangerous-triggers): say what the token and the secrets can do, and make a workflow without privileges pedantic #189) - Output clobbering (repro): untrusted
name=valuewritten to$GITHUB_OUTPUT(CodeQL). Today thegithub-envmessage presents GITHUB_OUTPUT as the safe channel. (feat(output-clobbering): report input with line breaks written to GITHUB_OUTPUT as a record #212, feat(environment-file-redirect): report writes to GITHUB_OUTPUT that lose the data #184) - Taint sources inside scripts:
gh pr view --json titleandgit log --format=%s(CodeQL models). Also follow PR refs from known actions (xt0rted/pull-request-comment-branch,github/branch-deploy). (feat(github-env): follow text that a script reads with gh and git #218) Not done: following pull request refs fromxt0rted/pull-request-comment-branchandgithub/branch-deploy. -
self-hosted-runner(repro): (feat(self-hosted-runner): count the configured labels and runner groups, and say when a pull request reaches the runner #194)- Treat the labels configured in
self-hosted-runner.labelsandruns-on: {group:}as self-hosted (zizmor#2350). - Move the finding to the default profile when a pull request event reaches the runner (poutine
pr_runs_on_self_hosted).
- Treat the labels configured in
-
unpinned-images: (feat(unpinned-images): check docker run images in scripts, add per-image policies and image allow/deny lists #211, feat(unpinned-images): pin container images to a digest with --online --fix=unsafe #239)- images in an object matrix,
matrix.cfg.image(repro, zizmor#2395) docker runanddocker pullin scripts (zizmor#738)- per-registry policies (zizmor#737)
- images in an object matrix,
-
cache-poisoning: addpnpm/setup,graalvm/setup-graalvmandramsey/composer-install. (feat(cache-poisoning): add pnpm/setup, graalvm and composer, and report caches of credential files #216) -
untrusted-artifact: addaochmann/actions-download-artifact,bettermarks/action-artifact-downloadandblablacar/action-download-last-artifact(repro). (feat(untrusted-artifact): know more download actions and report a download of every artifact with the option pedantic #219) - Secrets in a job's
env:reach every step: an option ofworkflow-secret-scope(ghalint 006, zizmor#1816). (feat(workflow-secret-scope): job-level secrets, secrets passed to reusable workflows, and unused workflow_call secrets #223) -
dependabot-missing-actions-update: report local composite actions thatdirectory: /does not reach (zizmor#1806). (feat(github-app): check two more token actions and cover local composite actions in dependabot-missing-actions-update #229) - Comment-triggered jobs gated only on the comment body: reuse the user check of
agentic-actions(zizmor#459). (feat(comment-trigger-authorization): report comment-triggered jobs that do not check who commented (pedantic) #217) -
github-app: also checktibdex/github-app-tokenandpeter-murray/workflow-application-token-action. (feat(github-app): check two more token actions and cover local composite actions in dependabot-missing-actions-update #229) - Pedantic candidates: (feat(unlocked-install): report npm, yarn and bun installs that run lifecycle scripts (pedantic) #187, feat(unlocked-install): report installs without a release-age cooldown (pedantic) #201, feat(use-trusted-publishing): report jobs that build, lack an environment or drop attestations (pedantic) #209, feat(comment-trigger-authorization): report comment-triggered jobs that do not check who commented (pedantic) #217, feat(unredacted-secrets): report values cut from a secret and printed by a script (pedantic) #222, feat(cache-poisoning): add pnpm/setup, graalvm and composer, and report caches of credential files #216, feat(workflow-secret-scope): job-level secrets, secrets passed to reusable workflows, and unused workflow_call secrets #223)
- npm installs without
--ignore-scripts(zizmor#1405) - installs without a release-age cooldown (#2044)
- trusted publishing in a job that also builds (#1968)
- caches holding credential files (#723)
- secrets passed to a reusable call (#1792)
- unused
workflow_call.secrets(#1044) - secrets derived in the shell and then printed
- npm installs without
4. New correctness checks
-
ghinrun:withoutGH_TOKEN(and withoutGH_REPOwhen there is no checkout) (repro; actionlint#246, ghlintMissingGhToken).superfluous-actionssends users togh. (feat(gh-without-token): report the gh command without GH_TOKEN or GITHUB_TOKEN #177) -
Malformed expression delimiters (repro; actionlint#417, #169, ghlint#144): (feat(malformed-expression-delimiters): report ${ github.ref }, $ {{ x }} and {{ github.sha }} #180)
${ github.ref }$ {{ x }}{{ github.sha }}(only with known context names)- a stray
}} $HOMEinwith:(pedantic)
-
>> GITHUB_OUTPUTwithout$, which writes a file in the workspace, and a>that overwrites an earlier write in the same script (repro). (feat(environment-file-redirect): report writes to GITHUB_OUTPUT that lose the data #184) -
Permissions too narrow for well-known actions (ghlint#285): (feat(permissions): report a job whose permissions are too narrow for its actions #192)
upload-sarifneedssecurity-events: writedeploy-pagesneedspagesandid-token- OIDC logins need
id-token
The same table could improve the
missing-permissionsfix. StepSecurity's knowledge base is AGPL, so the table has to be built independently. -
Concurrency deadlock when a caller and its callee resolve to the same group (repro; actionlint#538). (feat(concurrency-deadlock): report a group that a workflow shares with the workflow it calls #210)
-
A
run:written as a multi-line plain scalar (without|) is folded into one command (repro; actionlint#441). The fix adds|. (feat(multiline-plain-run): report a run folded into one line #205) -
github.event_name == 'x'where the workflow never receivesx(repro; actionlint#68). (feat(event-name-never-received): report github.event_name compared with an event the workflow never receives #214) -
Fail-fast inputs:
upload-artifactwithoutif-no-files-found: error, andsoftprops/action-gh-releasewithfail_on_unmatched_files: false(zizmor#1208, ghlint). (feat(fail-fast-action-inputs): report actions that pass when nothing was produced #196) -
Inputs that only work together:
download-artifactignoresrun-idwithoutgithub-token(ghlint#260). (feat(action-input-dependencies): report inputs of well-known actions that need each other #207) -
Later: (feat(undefined-property): check the properties of github.event against the webhook payloads #249, feat(macos-gnu-flags): report GNU-only options in run scripts of macOS jobs (pedantic) #241, feat(github-script-syntax): check the JavaScript of actions/github-script with node --check #242, feat(unmatched-paths-filter): report paths filters that match no file of the repository (pedantic) #238) Not done: array values in
matrix.include(#630); a real run accepted arrays, so the premise was wrong.- type
github.eventby the triggers of the workflow (actionlint#39) - GNU-only flags in scripts on macOS runners
node --checkforactions/github-script(actionlint#389)- array values in
matrix.include(#630) paths:globs that match no file (#430)
- type
5. Online
- Fetch the
action.ymlor reusable workflow of any remote action at the pinned ref, then run the existing input, output and secret checks on it (actionlint#383, #669 👍33, #648 👍30; ghalint exp validate-input). The same lookup enables: (feat(action): check the inputs of any remote action against its action.yml with --online #231, feat(workflow-call): check the calls of reusable workflows of other repositories with --online #236, feat(unpinnable-action): report a hash-pinned action whose own dependencies are not pinned #240) Not done: template injection in remote composite steps.- transitive pin checks (poutine
unpinnable_action); - finding node20 actions;
- template injection in remote composite steps.
- transitive pin checks (poutine
- Pinning comments: (feat(unpinned-uses): name the most specific version tag in the comment of --online --fix #224, feat(ref-version-mismatch): fix the version comment (unsafe) and read ratchet comments #227, feat(missing-version-comment): report a hash pin without a version comment and add it with --online --fix #232)
- The pin fix should write the most specific tag (
# v4.2.4, not# v4), like zizmor 1.26 and pinact. - Add a fix for
ref-version-mismatchthat rewrites the comment (unsafe). - Report a SHA pin with no comment (pedantic), and add the comment with
--online --fix.
- The pin fix should write the most specific tag (
- A release cooldown for actions: report a pinned tag younger than N days (pinact
--min-age). (feat(action-release-cooldown): report an action at a version younger than N days (online, opt-in) #235) - Pin
container:,services:anddocker://images to a digest through the registry API (ghalint#1044). (feat(unpinned-images): pin container images to a digest with --online --fix=unsafe #239) - Later: (feat(online): report the jobs whose environment has no required reviewers #247, feat(repojacking): report an action whose owner name anyone can register #248, feat(online): look up the actions of a GitHub Enterprise Server on github.com #250, feat(unpinned-uses): accept the tag of an immutable release under --online #251)
- check that an
environment:gate really has required reviewers (gato-x) - repojacking (octoscan)
- a github.com fallback on GHES (zizmor#735)
- accept tags of immutable releases (codeql#22674)
- check that an
6. CLI and integration
- A JSON Schema for
.github/jactionlint.yaml, generated from the rule registry, published to SchemaStore and referenced with a$schemaheader by--init-config. Another for--format json. (feat(config): JSON Schemas for the config file and for --format json #195) The SchemaStore submission is left to a person (steps inscripts/generate-config-schema/README.md). - Say when shellcheck or pyflakes did not run (actionlint#189, 9 👍). (feat(external-tools): say when shellcheck or pyflakes did not run #188)
- Exit 3 when an explicit path is missing.
- Add a coverage note to SARIF.
- Read a shellcheckrc and extra arguments from the config.
--norcis hard-coded today (actionlint#755, #486). (feat(shellcheck): read a shellcheckrc, extra arguments and optional checks from the config #191) - Built-in label prefixes for hosted-runner providers: Blacksmith, Depot, Ubicloud, WarpBuild, BuildJet, Namespace and RunsOn (repro; actionlint#499). (feat(runner-label): accept the labels of hosted-runner providers #199)
- A pre-commit hook with shellcheck bundled (actionlint#477, 10 👍). (fix(pre-commit): run the hooks for action.yml and dependabot.yml, add a bundled-shellcheck hook #181)
- Later: Done: shell completions (feat(cli): add shell completions and --explain RULE #204),
--selectand--no-ignores(feat(cli): add the options select, no-ignores and list-rules #200),--ruffand Jujutsu workspaces (feat(cli): add --ruff, --show-doc-urls, --finding-context, platform forgejo and jujutsu roots #215). Left open:--lspand linting.pre-commit-config.yaml(out of scope by decision) and auditing a remoteowner/repo@refdirectly.--lsp, with fixes as code actions- shell completions
--selectand--no-ignores- auditing a remote
owner/repo@refdirectly - linting
.pre-commit-config.yaml --ruff- Jujutsu workspaces (actionlint#752)
Suggested order
- Section 1, starting with the
Generateworkflow and the-latestlabels, which have to be done before 2026-10-19. Then the conformance test. - Section 2: the checkout protection, Node 20,
macos-14before 2026-11-02, andactions.lock. - The
untrusted-checkoutitems of section 3, where other tools are ahead. - The quick checks of section 4.
- Last, remote action metadata, the config JSON Schema and
--lsp.
For new checks, follow CONTRIBUTING.md: stable IDs, a documented tier and profile, positive and negative fixtures, and findings reviewed on real repositories. Keep noisy checks in the pedantic profile until there is evidence that they are precise.
AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5-5; version: claude-code_2-1-289_agent.
Activity
ci(generate): keep detecting new action releases from failing the dat…
fix(runner-label): follow the images behind the -latest labels (#171)
fix(action): check popular actions pinned to a full version or to a c…
fix(untrusted-checkout): follow pull/N/head fetches, skip merged pull…
docs: refresh the output of the github context example
fix(template-injection): keep the new sink checks out of the correctn…
feat(gh-without-token): report the gh command without GH_TOKEN or GIT…
fix(pre-commit): run the hooks for action.yml and dependabot.yml, add…
fix(parser): accept a whole mapping or list written as one expression…
feat(template-injection): report more free text of outsiders in the d…
chore(generate): refresh the generated data and keep merge_group dest…
feat(environment-file-redirect): report writes to GITHUB_OUTPUT that …
feat(untrusted-checkout): honor the fork pull request protection of a…
feat(untrusted-checkout): cover issue_comment workflows that build a …
96 remaining items
feat(unpinned-uses): accept a ref which the Actions lock file pins to…
feat(ref-version-mismatch): fix the version comment (unsafe) and read…
feat(workflow-call): check the calls of reusable workflows of other r…
feat(unpinnable-action): report a hash-pinned action whose own depend…
feat(online): look up the actions of a GitHub Enterprise Server on gi…
fix(if-always-true): do not report the newlines at the end of a block…
feat(comment-trigger-authorization): report comment-triggered jobs th…
feat(actions-lock): lint GitHub's Actions lock file against the workf…
feat(unmatched-paths-filter): report paths filters that match no file…
feat(missing-version-comment): report a hash pin without a version co…
feat(github-app): check two more token actions and cover local compos…
feat(action-release-cooldown): report an action at a version younger …
feat(macos-gnu-flags): report GNU-only options in run scripts of macO…
jdx commentedon Oct 13, 2026
Closing: the work is merged (56 of 57 boxes ticked, each with its PRs). The remaining items are not planned:
--lspand linting.pre-commit-config.yaml(out of scope by decision)owner/repo@refdirectlygithub.job_workflow_sha, following pull request refs fromxt0rted/pull-request-comment-branchandgithub/branch-deploy, array values inmatrix.include, template injection in remote composite steps, the SchemaStore submission)Changes that would break users or Go importers are collected in
v3.md(#264).AI-assisted — Tool: Claude Code; model: anthropic/claude-sonnet-5-5.