GitHub Actions is not just automation. It is part of your software supply chain.
Your workflows decide:
- which code gets executed
- which tokens and secrets are available
- which packages get installed
- which artifacts get published
- which cloud credentials can be minted
That makes workflow security a pull-request concern, not only a platform checklist.
Skylos scans GitHub Actions workflows alongside application code so risky CI/CD changes show up before merge.
What Skylos Checks In GitHub Actions
Skylos looks for workflow patterns that are easy to miss in review but serious when combined with secrets, publishing, or untrusted pull requests.
Privileged Triggers
Some events execute with more trust than developers expect.
Skylos flags risky triggers such as:
pull_request_targetworkflow_run
These are not always wrong. They become dangerous when the workflow checks out or executes code influenced by an untrusted contributor.
on:
pull_request_target:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm test
The safer pattern is to keep untrusted pull-request execution separate from jobs that can write to the repository, access secrets, or publish artifacts. Since v7, actions/checkout refuses to check out a fork's pull request under pull_request_target or workflow_run unless you set allow-unsafe-pr-checkout: true (checkout v7 notes). Older majors (and SHA pins to them) don't have that guard, and it doesn't cover branches pushed to the same repository, such as an agent's branch.
Broad GITHUB_TOKEN Permissions
GitHub Actions permissions should be scoped to the job.
Skylos flags broad permissions such as:
- workflows with no
permissionsblock, so every job gets the default token (the most common case) permissions: write-allorread-all, at workflow or job level- write permissions set at workflow level, which apply to every job
permissions: write-all
For most analysis jobs, start small:
permissions:
contents: read
Then add only the permission the job actually needs.
Unpinned Actions And Reusable Workflows
Tags such as @v4 are convenient, but they are still moving references.
Skylos flags every uses: reference that isn't a full commit SHA (SKY-D292), first-party actions/* included, so reviewers can see where the workflow depends on mutable code.
steps:
- uses: third-party/example-action@v1
For sensitive workflows, pin third-party actions to a full commit SHA.
steps:
- uses: third-party/example-action@2d3f7a9d1b4c0a6e9f8b7c5d4e3a2f1b0c9d8e7f
This matters most for release, deploy, package publishing, and permission-bearing workflows.
Unsafe actions/checkout Defaults
actions/checkout persists credentials by default unless configured otherwise.
Skylos flags checkout steps that omit:
with:
persist-credentials: false
That setting is especially important when a job later runs third-party code, dependency scripts, or commands that do not need repository write credentials.
Template Injection In Shell And Action Inputs
GitHub expression interpolation can turn untrusted event data into shell input.
Skylos flags risky use of event-controlled values in places such as:
run:commands- known command-like action inputs
- container and service options
- run: echo "${{ github.event.pull_request.title }}"
The safer pattern is to pass untrusted values through environment variables and quote them carefully inside the shell.
Self-Hosted And Dynamic Runners
Self-hosted runners are powerful because they can reach internal systems. That also makes them risky.
Skylos flags jobs that use:
self-hosted- dynamic runner labels
- expression-derived runner selection
runs-on: ${{ inputs.runner }}
Dynamic runner selection should be treated as sensitive configuration, especially in reusable workflows.
Unpinned Containers And Docker Images
Workflows often pull Docker images directly through:
- job containers
- service containers
docker://actionsdocker pulldocker run
Skylos flags image references that are not pinned by digest.
container:
image: node:latest
Prefer digest-pinned images for sensitive jobs:
container:
image: node@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
Secret Exposure And Inheritance
Secrets should be scoped to the smallest surface area possible.
Skylos flags patterns such as:
- reusable workflows with
secrets: inherit - broad secret env variables at workflow or job scope
- dynamic secret lookup with
secrets[...] toJSON(secrets)- jobs that use secrets without an environment boundary
jobs:
deploy:
uses: org/infra/.github/workflows/deploy.yml@<full-commit-sha>
secrets: inherit
Secret inheritance is convenient, but it can make a reusable workflow much harder to reason about. (GitHub only allows secrets: inherit on jobs that call a reusable workflow, and that is the case SKY-D297 checks.)
Unsafe Writes To $GITHUB_ENV And $GITHUB_PATH
Writing untrusted data to GitHub's environment files can affect later steps in the same job.
Skylos flags unsafe writes such as:
- run: echo "NAME=${{ github.event.issue.title }}" >> "$GITHUB_ENV"
Treat environment-file writes as privileged behavior. Keep untrusted values out unless they are normalized first.
GitHub App Token Misuse
GitHub App tokens are powerful when they can write repository content, open pull requests, or trigger workflows.
Skylos flags risky actions/create-github-app-token usage when token permissions or repository scoping are too broad for the job.
The scanner cannot know every organizational policy, but it can make dangerous defaults visible during review.
Spoofable Actor Checks
Workflows sometimes trust users by checking string values such as:
if: github.actor == 'dependabot[bot]'
Skylos flags conditions that trust a bot identity through the actor context (SKY-D304) — use event-specific sender IDs where possible — and contains() checks whose second argument an attacker controls, such as contains('alice,bob', github.actor) (SKY-D303).
If a job controls releases, secrets, or repository writes, use explicit allowlists and platform permissions instead.
Release Jobs With OIDC And Repo-Controlled Scripts
OIDC is better than long-lived cloud secrets, but it still creates a credential minting path.
Skylos flags jobs that request:
permissions:
id-token: write
and then invoke repo-controlled release or build scripts.
That does not mean the job is wrong. It means the script becomes part of the trust boundary and deserves careful review.
Package Installs That Run Lifecycle Scripts
JavaScript package managers can run lifecycle scripts during install.
Skylos flags every npm, pnpm, yarn or bun install/ci step that doesn't pass --ignore-scripts (SKY-D312).
- run: npm ci
Safer baseline:
- run: npm ci --ignore-scripts
When a workflow truly needs lifecycle scripts, that exception should be intentional.
Missing Timeouts On Privileged Jobs
Privileged jobs should not run forever.
Skylos flags jobs without timeout-minutes when the workflow is release-like (a release trigger or tag pushes) or uses pull_request_target/workflow_run, or when the job can write to the repository or mint an OIDC token (SKY-D313).
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- run: npm publish
Add an explicit timeout:
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 15
Rule IDs
| Check | Rule |
|---|---|
| Privileged triggers | SKY-D290 |
| Missing or broad token permissions | SKY-D291 |
| Unpinned actions and reusable workflows | SKY-D292 |
| Checkout persists credentials | SKY-D293 |
| Expression injection | SKY-D294 |
| Self-hosted or dynamic runners | SKY-D295 |
| Unpinned containers | SKY-D296 |
secrets: inherit | SKY-D297 |
| Broad secret exposure | SKY-D298, SKY-D299, SKY-D309 |
Unsafe $GITHUB_ENV / $GITHUB_PATH writes | SKY-D300 |
| GitHub App token scope | SKY-D302 |
Spoofable contains() and bot checks | SKY-D303, SKY-D304 |
| OIDC token exposed to repo scripts | SKY-D310 |
| JavaScript install scripts | SKY-D312 |
| Privileged job without a timeout | SKY-D313 |
The scanner also reports SKY-D301 and SKY-D305 to SKY-D308, and SKY-D311. Workflow checks run with --danger (or -a). The same scan checks the root .gitlab-ci.yml (SKY-D314–D323) and risky shell commands inside CI steps, such as data exfiltration (SKY-D327) or a remote script piped to a shell (SKY-D328).
Why Scan Workflows And Code Together
Workflow-only scanning catches CI/CD misconfiguration.
Code scanning catches application bugs.
Real incidents often need both views:
- a workflow runs on the wrong trigger
- the job has broad token permissions
- the job installs untrusted dependencies
- the repository contains a dangerous release script
- a pull request changes both workflow and code in one diff
Skylos is designed for that combined review path. The goal is to show the reviewer when a pull request changes the security properties of the repository, not just whether a single file has a known bad pattern.
How To Run It
Run Skylos locally first:
pip install skylos
skylos . --danger --secrets --quality
Rule details: GitHub Actions security docs.
Then add it to CI as a pull-request gate:
name: skylos-security
on:
pull_request:
branches: [main]
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install Skylos
run: python -I -m pip install "skylos==5.1.1"
- name: Scan repository
run: skylos . --danger --secrets --quality --strict
--strict makes this example fail when the scan emits any finding. If your repository already has an accepted baseline, replace it with --gate and set explicit thresholds in pyproject.toml.
For Python-specific setup, use:
What To Fix First
Prioritize findings in this order:
- Privileged triggers that execute untrusted code
- Broad write permissions on pull-request workflows
- Release jobs with secrets, OIDC, or package publish access
- Unpinned third-party actions and containers
- Secret inheritance or workflow-level secret exposure
- Dependency installs that execute lifecycle scripts
- Missing timeouts and unsafe artifact behavior
That order keeps attention on the paths where a workflow can write, publish, or mint credentials.
When an AI agent edits a workflow
skylos done --base origin/main flags a change to the CI workflow that runs Skylos or to [tool.skylos] (SKY-A114) and CI test steps loosened so failures still pass (SKY-A112).
Where Skylos Fits
If you already use a dedicated GitHub Actions linter, keep it.
Skylos is useful when you want one review signal for:
- workflow security
- Python security
- secret exposure
- dead code
- AI-generated code regressions
- CI gate enforcement
That makes it a good fit for teams that want GitHub Actions security checks and application security checks in the same pull request workflow.
Running this across a team? These checks are free in the CLI. Skylos Cloud keeps the results in one place: the Free plan adds a pass/fail check on pull requests and scan history for one project, and the Workspace plan adds up to 10 projects, approvals for findings you decide not to fix, pull request comments, and audit exports.