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_target
  • workflow_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 permissions block, so every job gets the default token (the most common case)
  • permissions: write-all or read-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:// actions
  • docker pull
  • docker 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

CheckRule
Privileged triggersSKY-D290
Missing or broad token permissionsSKY-D291
Unpinned actions and reusable workflowsSKY-D292
Checkout persists credentialsSKY-D293
Expression injectionSKY-D294
Self-hosted or dynamic runnersSKY-D295
Unpinned containersSKY-D296
secrets: inheritSKY-D297
Broad secret exposureSKY-D298, SKY-D299, SKY-D309
Unsafe $GITHUB_ENV / $GITHUB_PATH writesSKY-D300
GitHub App token scopeSKY-D302
Spoofable contains() and bot checksSKY-D303, SKY-D304
OIDC token exposed to repo scriptsSKY-D310
JavaScript install scriptsSKY-D312
Privileged job without a timeoutSKY-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:

  1. Privileged triggers that execute untrusted code
  2. Broad write permissions on pull-request workflows
  3. Release jobs with secrets, OIDC, or package publish access
  4. Unpinned third-party actions and containers
  5. Secret inheritance or workflow-level secret exposure
  6. Dependency installs that execute lifecycle scripts
  7. 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.