Configuration
This document describes how to configure jactionlint behavior.
Note that configuration file is optional. Running jactionlint without a configuration file works fine in most cases: the default profile is on, which has the correctness checks and the security and policy checks that are worth failing a build on. A configuration file is for the things that only you can know (your self-hosted runner labels, your ignore patterns), for choosing another profile (correctness is what actionlint checks, pedantic adds the noisy and opinionated rules) and for tuning single rules (rules:). See the policy for new checks for the three tiers and how a check gets into a profile.
Configuration file
Configuration file jactionlint.yaml or jactionlint.yml can be put in .github directory.
NOTE
The file names used by the original actionlint (.github/actionlint.yaml and .github/actionlint.yml) are also accepted, so an existing configuration keeps working. When both exist, jactionlint.yaml is used first. jactionlint --init-config generates .github/jactionlint.yaml.
Note: If you're using Super-Linter, the file should be placed in a different directory. Please check the project's document.
# Configuration related to self-hosted runner.
self-hosted-runner:
# Labels of self-hosted runner in array of strings.
labels:
- linux.2xlarge
- windows-latest-xl
- linux-multi-gpu
# When true, only the labels listed above are accepted at 'runs-on:'. Built-in labels such as 'ubuntu-latest' and
# 'self-hosted' are reported unless listed. (default: false)
strict-labels: false
# Configuration variables in array of strings defined in your repository or organization.
config-variables:
- DEFAULT_RUNNER
- JOB_NAME
- ENVIRONMENT_STAGE
# Actions (or reusable workflows) which must be used in every workflow. Opt-in; disabled by default.
required-actions:
# Any version of the action is accepted when 'version' is omitted.
- action: actions/checkout
# 'version' is the exact ref after '@'. Tags, branches and commit SHAs are compared as-is.
- action: github/codeql-action/init
version: v3
# Controls what permissions are assumed for a caller workflow that declares no
# "permissions:" block at all when checking caller/callee permissions for local
# reusable workflow calls. The default token is a setting of the repository, so
# when this is not set only a scope that no default token has ("id-token") is
# reported. "restricted" assumes GitHub's restricted default token. "permissive"
# assumes write on every scope except "id-token", which always requires an
# explicit opt-in regardless of repo settings.
assume-default-permissions: restricted
# Secrets in array of strings defined in your repository or organization.
config-secrets:
- DEPLOY_TOKEN
- API_KEY
- ACCESS_TOKEN
# Path-specific configurations.
paths:
# Glob pattern relative to the repository root for matching files. The path separator is always '/'.
# This example configures any YAML file under the '.github/workflows/' directory.
.github/workflows/**/*.{yml,yaml}:
# List of rule IDs and regular expressions to filter errors. A rule ID ignores all errors of the rule, and a
# regular expression is matched to the error messages.
ignore:
# Ignore all errors of the rule
- unpinned-uses
# Ignore the specific error from shellcheck
- "shellcheck reported issue in this script: SC2086:.+"
# This pattern only matches '.github/workflows/release.yaml' file.
.github/workflows/release.yaml:
ignore:
# Ignore errors from the old runner check. This may be useful for (outdated) self-hosted runner environment.
- 'the runner of ".+" action is too old to run on GitHub Actions'
# Durable ignores: accept findings by rule and by where they are, not by line.
ignores:
- rule: unpinned-uses
uses: actions/checkout
file: .github/workflows/release.yaml
reason: pinned by an organization ruleset
expires: 2027-06-30
# Profile: the set of rules which are enabled. 'correctness', 'default' or 'pedantic'. (default: default)
profile: pedantic
# The level of each rule by its ID: 'error', 'warn', 'info' or 'off'. See https://jactionlint.jdx.dev/rules
rules:
# Lower the level of a rule enabled by the profile
unpinned-uses: warn
# Turn a rule off
missing-permissions: off
# Turn on a rule which the profile does not enable
require-shell: error
# A rule with options takes a mapping
max-run-lines:
level: warn
max: 30
timeout-too-long:
max: 60
# Config files to inherit from. Relative paths are resolved from this file. Later files win.
extends:
- ../shared/jactionlint.yamlself-hosted-runner: Configuration for your self-hosted runner environment.labels: Label names added to your self-hosted runners as list of pattern. Glob syntax supported bypath.Matchis available.strict-labels: Whentrue, the runner label check accepts only the labels listed inlabels(glob patterns supported). The built-in labels (GitHub-hosted runner labels such asubuntu-latestand the preset self-hosted labels such asself-hosted,linux, andx64) are reported as errors unless they are listed inlabels. This is useful when all jobs must run on your own runners, or when your runners (e.g. Actions Runner Controller runner sets) do not have the defaultself-hostedlabel. Label conflict checks still apply to listed built-in labels. The default isfalse.
config-variables: Configuration variables. When an array is set, jactionlint will checkvarsproperties strictly. An empty array means no variable is allowed. The default valuenulldisables the check.required-actions: List of actions which must be used in each checked workflow. This check is disabled unless the list is non-empty. A missing action is reported once per workflow at the position of its first job.action: Name of the action likeactions/checkout, without@version. A sub-path is part of the name (github/codeql-action/init). Case-insensitive. Reusable workflow calls (jobs.<id>.uses) are also matched.version: Optional ref (tag, branch or commit SHA) compared exactly. The requirement is satisfied when at least one use of the action has this version. When omitted any version is accepted.
Local actions (
./...and$/...) and Docker images (docker://...) never match. Only workflow files are checked; the steps of composite action files (action.yml) are not (see composite actions).assume-default-permissions: Controls how the caller/callee permissions check for local reusable workflow calls treats a caller workflow that has nopermissions:block at the workflow level and nopermissions:block on the calling job. This mirrors the repository-level "Workflow permissions" setting (Settings → Actions → General), which jactionlint cannot read from the workflow file. When the option is not set, nothing is assumed: only a scope that no default token has (id-token, which always requires an explicit opt-in) is reported. Set torestrictedto assume GitHub's restricted default token (contents: readandpackages: read, everything elsenone) and report every other scope the called workflow needs. Set topermissiveto assume the permissive default (write on every scope);id-tokenis still treated asnonethere. Note: this only affects callers with nopermissions:block anywhere. Once a caller declares anypermissions:block — evenpermissions: {}— the check always runs against that explicit block, because GitHub treats any scope omitted from an explicit block asnone.generated-files: What to do with a file whose first comment says that a tool wrote it, such as# This file was automatically generated by gh-aw. DO NOT EDIT.,# generated by praktikaor# Generated by projen. Only the comment block at the top of the file counts, and the comment has to be a generator marker: it starts with the wording (automatically generated,generated by,managed by modulesync) or is onlyDO NOT EDIT. A sentence such as# do not edit the matrix by handor# generated by handis not a marker. The author of such a file fixes the generator, not the file, so by default (skip-policy) the rules of thepolicyandstylegroups (missing-timeout,unused-job-output,anonymous-definition,unpinned-usesand so on, see the rules) are not reported there, while thecorrectnessandsecurityrules still are.reporttreats the file like any other file, andskipreports nothing in it. Ignore comments andignoresentries of the rules that are not reported are not flagged byunused-ignoreeither.config-secrets: Secrets. When an array is set, jactionlint will checksecretsproperties strictly against the list. An empty array means no secret is allowed. The default valuenulldisables the check.GITHUB_TOKENis always allowed. Note: this check only applies when secrets are not explicitly declared in the workflow (e.g. viasecrets:inon.workflow_call), since declared secrets are already checked by their type.paths: Configurations for specific file path patterns. This is a mapping from a glob pattern and the corresponding configuration.{glob}: A file path glob pattern to apply the configuration. The path separator is always '/'. It is matched to the relative path from the repository root. For example.github/workflows/**/*.yamlmatches all the workflow files (with.yamlfile extension). For the glob syntax, please read the doublestar library's documentation.ignore: The configuration to ignore (filter) the errors. This is an array of rule IDs and regular expressions. A rule ID ignores all the errors of the rule. A regular expression ignores the errors whose message matches it. It's similar to the--ignorecommand line option.
ignores: Findings to accept, matched by rule and by where they are. See Durable ignores.online: Turns on the online checks for the files this configuration applies to, like the--onlineflag does for the whole run. They query the GitHub API. The default isfalse: nothing uses the network. The environment variableJACTIONLINT_ONLINE(1, a mode, or0) overrides this key, and the--onlineand--no-onlineflags override both.online-options: Tunes the online checks. Every key is optional. They apply to the whole run, decided by the first file checked, and the command line flags win over them. Amodeofcacheorstrictalso turns the online checks on, unless the configuration saysonline: falseexplicitly: an explicitfalsewins over the mode. See the usage document for what each does.mode:cache(never use the network, answer from the cache),strict(a skipped lookup makes the exit status 3) orcache,strict. Same as--online=MODE.api-url: The REST API of a GitHub Enterprise Server such ashttps://ghe.example.com/api/v3. Same as--online-api-url. A token is not sent to a host named here when the file is a repository's.github/jactionlint.yaml, only when it is your user-global config or the file of--config-file.token-env,token-file: The variable and the file which hold the token, read beforeGITHUB_TOKENandGH_TOKEN.allow,deny: Patternsowner/repowith*wildcards (mycorp/*,*/setup-*; case does not matter) of the repositories which may or may not be looked up.denywins.cache-ttl(default1h),max-rate-limit-wait(default30s;0snever waits),retries(default2),concurrency(default6, at most32) andgh-cli(defaulttrue: askgh auth tokenwhen no variable or file has a token).
yamlonline: true online-options: deny: ["mycorp/*"] # internal actions GitHub cannot answer for max-rate-limit-wait: 10sbaseline: Hides the findings recorded in a baseline file so that a repository can adopt the checks gradually.auto(ortrue) applies.github/jactionlint-baseline.jsonwhen the file exists,falseor no key applies no baseline, and any other value is the path of the baseline file relative to the repository root, which must exist. The--baselineflag overrides it (--no-baselineturns it off). The file is written byjactionlint --baseline-write.fix: Configuration of--fix.rules: A list of rule IDs.--fixapplies only the fixes of these rules, like--fix --fix-rules a,bon the command line (which overrides it). The default, an empty list, applies the fixes of every rule. Unknown IDs are errors.
profile,rulesandextends: See Profiles, Rules and Extending config files.
Unknown keys are errors. jactionlint reports the key with its position and suggests the closest known key when it looks like a typo:
unknown key "self-hosted-runnr" in the configuration at line 3, column 1. did you mean "self-hosted-runner"?Profiles
A profile is a named set of rules which are enabled together. profile selects one of them:
| Profile | Enables |
|---|---|
correctness | What actionlint checks (syntax, expressions, actions, workflow_call, permissions, credentials, events, globs, runner labels, shell names, IDs, needs, matrix, if:, shellcheck, pyflakes), template-injection (untrusted input in scripts, in container: options and in the prompts of AI agent actions), and the bug detectors of jactionlint such as unsound-ternary, workflow-run-names, local-action-checkout, action-syntax and dependabot-syntax. No security posture or policy rule. |
default | correctness plus the security and policy rules worth failing a build on: pinned actions and images, permissions, timeouts, concurrency, dangerous triggers, artifact and cache poisoning, unverified downloads, trusted publishing and so on. Used when profile is omitted. |
pedantic | default plus the noisy and opinionated rules (require-shell, max-run-lines, anonymous-definition, self-hosted-runner, unused-needs and so on) and the pedantic findings of the audits that have them (see below). |
Each profile includes the rules of the profile before it. --profile NAME on the command line overrides profile of the configuration file. Every rule of the correctness and default profiles reports at the level error; the rules of the pedantic profile keep their own levels (info for self-hosted-runner, for example).
strict and all, the names of the profiles before 2.0, still work for one minor version and mean pedantic, with a deprecation warning. jactionlint --migrate-config rewrites them.
A few audits have a noisier tier of findings, like the pedantic persona of zizmor. They are one rule with one ID, and the option pedantic turns the tier on. Unset, it is true under the pedantic profile and false otherwise, so rules: {template-injection: {pedantic: true}} reports the pedantic findings of that rule under the default profile and pedantic: false turns them off under the pedantic one.
The profile of each rule is in the list of rules. Some rules belong to no profile and run only when the configuration turns them on: required-actions (when the required-actions list is not empty) and timeout-too-long (when max is set). The online rules do not follow a profile either: they run, at their own level, when the online checks are on.
Rules
rules sets the level of each rule by its stable rule ID. The level is one of:
error: The finding is printed and makes jactionlint exit with status 1.warnandinfo: The finding is printed with awarning:orinfo:prefix (or the corresponding level in--format sarif,gccandgithub). It does not change the exit status unless--strict-exitis given.off: The rule is disabled.
A rule not listed in rules follows the profile: it runs at its default level if the profile includes it and is off otherwise. Every rule is an error by default. Unknown rule IDs are errors with a suggestion for a similar ID.
A rule with options takes a mapping with level and the options. Giving options without level enables the rule at its default level:
rules:
max-run-lines:
level: warn
max: 80
timeout-too-long:
max: 60| Rule | Option | Description |
|---|---|---|
max-run-lines | max | Maximum number of non-blank lines in a run: script. Default 100 when the rule is enabled by the pedantic profile. |
missing-timeout | default-minutes | The timeout-minutes which --fix adds to a job without one. There is no default: without it the rule has no fix. Lowered to max of timeout-too-long when that is smaller. |
timeout-too-long | max | Maximum allowed timeout-minutes of a job in minutes. Values given by ${{ }} are not checked. The rule does nothing without max. |
forbidden-uses | allow | List of patterns of the only actions and reusable workflows that may be used, e.g. actions/*. See forbidden actions. |
forbidden-uses | deny | List of patterns of actions and reusable workflows that must not be used. The rule does nothing without allow or deny. |
secrets-outside-env | allow | List of secret names that may be used by a job without an environment:. GITHUB_TOKEN is always allowed. |
typosquat-uses | allow | List of owner/repo slugs that are never reported, e.g. a legitimate fork of a popular action. |
impostor-commit | max-branches | How many branches of an action repository a pinned commit is compared with before giving up without a verdict. Default 1000; without a token at most 100 are compared. |
known-vulnerable-actions | allow | List of advisory IDs (GHSA-...) which are not reported: allow: [GHSA-mrrh-fwg8-r2c3]. Default none. |
mutable-runner-label | pin | Mapping from a moving label to the fixed label that --fix writes in its place (ubuntu-latest: ubuntu-24.04). There is no default: without an entry the finding has no fix. |
continue-on-error | steps | true also reports steps with continue-on-error: true. Default false: only jobs are reported. |
Extending config files
extends lists config files to inherit from. This makes an organization-wide configuration possible: keep a shared file in a repository or a submodule and extend it from every project.
extends:
- ../shared/jactionlint.yaml
- /etc/jactionlint/org.yaml
profile: pedantic
rules:
require-shell: off- Relative paths are resolved from the directory of the file which lists them.
extendsis read only from config files (not fromjactionlintAPI calls that parse bytes). - Later files win over earlier ones, and the file itself wins over all of them.
profileand scalar keys are replaced.rulesandpathsare merged by key: the entry of the later file replaces the entry with the same rule ID or the same glob. Lists such aslabels,config-variablesandrequired-actionsare replaced.- A file can extend other files. A cycle, or a chain deeper than 10 files, is an error.
Ignoring errors by rule ID
The ignore lists of paths, the --ignore command line option and the # jactionlint ignore= comments take rule IDs as well as regular expressions. A pattern which is exactly a rule ID ignores all the errors of the rule; any other pattern is a regular expression matched to the error messages. See the usage document.
The unused-ignore rule (in the pedantic profile) reports ignore comments which did not suppress anything.
Durable ignores
An ignore comment (# jactionlint ignore=...) lives on the line it covers, so a tool that rewrites the line takes it along: when Renovate or Dependabot bumps uses: actions/checkout@<sha> # v4.1.0, the trailing comment is replaced with the new version and the ignore is gone. The ignores list of the config file does not have this problem. An entry says which rule to ignore and describes the place by what the workflow says, not by its position:
ignores:
- rule: unpinned-uses # a rule ID, or a list of IDs: [unpinned-uses, artipacked]
uses: actions/checkout # the action, whatever its ref is
file: .github/workflows/*.yaml # optional glob
job: release # optional job ID
step: checkout # optional step id or name
reason: pinned by an organization ruleset
expires: 2027-06-30| Key | Meaning |
|---|---|
rule | Required. A rule ID or a list of them. An unknown ID is an error. |
file | A glob matched to the path relative to the project root (and to the path as printed). ** crosses directories. The separator is always /. |
job | The ID of the job (its key under jobs), without regard to case. Matches findings anywhere in the job, including its steps. |
step | The id or the name of the step, compared as written. Matches findings anywhere in the step. |
uses | The uses: value of the step, or of the job when it calls a reusable workflow. See below. |
reason | Why the finding is accepted. It is free text for the readers of the file and appears in the messages about the entry. |
expires | YYYY-MM-DD. The entry suppresses through the end of that day (UTC) and stops after it. |
An entry needs at least one of file, uses, job and step; to turn a rule off everywhere, use rules. All the keys an entry sets must match. Entries are not combined: two entries are two independent reasons to ignore.
uses takes one of three forms:
- A name pattern like the ones of
forbidden-uses:actions/checkout(the root action of the repository),actions/*(every action of the owner, including the ones in directories),owner/repo/*,owner/repo/sub. Names are case-insensitive. Without@refany ref matches, so the entry keeps working when a tool changes the tag or SHA; with@ref(actions/checkout@v4) only that exact ref matches. - A glob with
*for values which are not repository references:docker://alpine*,./.github/actions/*. A local path can be written./pathor$/path; the entry matches both spellings. - A regular expression between slashes, matched to the whole
uses:value (not anchored unless you write^or$):/^actions\/(checkout|cache)@/. The syntax is RE2.
How a finding gets its attributes: jactionlint finds the job and the step whose YAML block contains the line of the finding, the same blocks an ignore comment covers. A finding on the uses: line of a step belongs to that step and job; a finding on the jobs.<id> line or on the job's runs-on belongs to the job and to no step; a finding about the workflow as a whole (permissions, on) belongs to neither, so only rule and file can match it. When the structure is ambiguous, for example two steps in one flow-style line (steps: [{uses: a}, {uses: b}]), the step is unknown and an entry that asks for step or uses does not match. The steps of a parallel: group are steps like the others: an entry finds them by their uses and by their id or name. An ignore never suppresses on a guess.
Expiry and unused entries
- On the day after
expiresthe entry stops suppressing: the findings it hid come back, and the entry itself is reported asexpired-ignore(an error, in thecorrectnessprofile) at its position in the config file, with itsreason. Either fix the findings or renew the date. - During the last 14 days the entry still works and is reported as
expired-ignorewith levelinfo, so you are warned before the findings come back. An entry that is about to expire and suppresses nothing is also reported asunused-ignore. - An entry which suppressed nothing is reported as
unused-ignore(the same rule as the unused ignore comments, so it is in thepedanticprofile) at its position in the config file. Because a pre-commit hook lints only the changed files, an entry is called unused only when every file it could apply to was linted in the run. A run that lints a single file never reports an entry that also applies to others. An entry for a rule that did not run (it is off, an online rule without the online checks, orshellcheckandpyflakeswithout their command) is not unused. The findings about the entries follow--ignoreand--fixlike any other finding.
Entries of config files listed in extends are added to the entries of the file that extends them (extended files first), and a finding about an entry is reported in the file that contains it.
LinterOptions.Now replaces the clock used for the expiry, for tests. In the Go API, Config.Ignores holds the entries as ConfigIgnore values, and ConfigIgnore.Snippet renders one as YAML for tools that write entries (for instance a migration of # zizmor: ignore[...] comments).
Deprecated keys
The following keys were replaced by rules. They still work for now: jactionlint translates them into rules and prints a deprecation warning to stderr once per config file (in --format sarif it is in the log's toolConfigurationNotifications). jactionlint --migrate-config rewrites the file (keeping the comments and the other keys) into the rules mapping.
| Deprecated key | Replacement |
|---|---|
require-commit-hash: true | rules: {unpinned-uses: error} |
require-permissions: true | rules: {missing-permissions: error} |
require-checkout-before-local-action: true | rules: {local-action-checkout: error} (on by default now) |
require-expression-wrapping: true | rules: {require-expression-wrapping: error} |
check-falsy-ternary: true | rules: {unsound-ternary: error} (on by default now) |
check-workflow-run-names: true | rules: {workflow-run-names: error} (on by default now) |
require-shell: true | rules: {require-shell: error} |
max-run-lines: N | rules: {max-run-lines: {level: error, max: N}} |
timeout-minutes: {required: true} | rules: {missing-timeout: error} |
timeout-minutes: {required: false} | rules: {missing-timeout: off} |
timeout-minutes: {max: N} | rules: {timeout-too-long: {level: error, max: N}} (missing-timeout is left to the profile) |
Setting one of these keys to false (or max-run-lines: 0) turns the rule off, which is the way to disable the three rules which are on by default in the old format. A rule written in rules wins over the deprecated key.
Configuration file location and priority
jactionlint looks for a configuration file in the following order and uses the first one found. Configurations are not merged:
- The file passed via the
--config-filecommand line option. jactionlint.yaml(orjactionlint.yml) in the repository's.githubdirectory, thenactionlint.yaml(oractionlint.yml) in the same directory as used by the original actionlint. jactionlint locates the project by searching upwards from the linted file's directory.- The user-global configuration at
$XDG_CONFIG_HOME/jactionlint/jactionlint.yaml(orjactionlint.yml). When$XDG_CONFIG_HOMEis not set,$HOME/.config/jactionlint/jactionlint.yamlis used instead, following the XDG Base Directory specification. The location used by the original actionlint ($XDG_CONFIG_HOME/actionlint/actionlint.yaml) is checked after it.
The user-global configuration is useful for personal or organization-wide defaults shared across many repositories (for example via dotfiles), or for CI base images that need a baseline configuration without injecting a config file into every checkout. A repository's own .github/jactionlint.yaml always takes precedence over the global configuration, so per-repository settings are never overridden by the global defaults.
$XDG_CONFIG_HOME must be an absolute path. A relative path is ignored as the specification requires. $HOME/.config is used on all platforms including Windows and macOS (%USERPROFILE%\.config on Windows). The global configuration is not used when --config-file is given.
Generate the initial configuration
You don't need to write the first configuration file by your hand. jactionlint command can generate a default configuration with --init-config flag.
jactionlint --init-config
vim .github/jactionlint.yamlTo rewrite an existing configuration which uses the deprecated keys:
jactionlint --migrate-configChecks | Rules | Installation | Usage | Go API | References