# ci-secure (/skills/ci-secure)



[ci-secure](https://github.com/starslingdev/skills) is a free, open-source, MIT-licensed agent skill that runs on your machine, scans a repository's GitHub Actions workflows for the ten critical CI/CD attack vectors, and offers your coding agent a fix for every finding. It works independently of StarSling Runners and does not require the StarSling GitHub App.

[See the ci-secure overview](https://starsling.dev/ci-secure) for a quick introduction before following the operational guide below.

<Callout type="warn">
  ci-secure is deliberately **not** a comprehensive security audit. It checks critical exploit-chain vectors only — complete outsider-to-compromise paths with real incidents behind them. A clean ci-secure report means those ten paths are closed, not that your CI is secure. The [selection criteria and the rejection record](https://github.com/starslingdev/skills/blob/main/skills/ci-secure/references/why-these-ten.md) document what was left out and why.
</Callout>

<Callout type="info">
  ci-secure scans for attack vectors. To grade your workflow configuration against CI best practices, use [ci-score](https://docs.starsling.dev/skills/ci-score); to find what actually makes your CI slow, use [ci-speedup](https://docs.starsling.dev/skills/ci-speedup). All three are local, on-demand skills; [StarSling optimization PRs](https://docs.starsling.dev/ai-agents/optimizations) are a separate paid, hosted service.
</Callout>

## Before you start

You need:

* A full local checkout of the repository you want to scan, with a `.github/workflows/` directory. The skill stops early if the repo has no GitHub Actions workflows.
* Python 3.9 or newer and [PyYAML](https://pypi.org/project/PyYAML/) — the scanner's only third-party dependency.
* Node.js and npm to install or invoke the skill with `npx`.

The [GitHub CLI](https://cli.github.com/) is **optional but recommended**. Four checks need it: the impostor-SHA vector, which cannot be answered from YAML alone, the dormancy note on findings, and two repository settings read over the API. Without `gh`, the scan still runs — the impostor-SHA check reports as skipped, and the two API-gated settings report as unmeasured coverage gaps rather than as passes.

## Install and run

Install the skill with Vercel's Skills CLI:

```bash
npx skills add starslingdev/skills
```

The CLI lets you select the coding agent and whether to install the skill for the current project or globally. It [supports a long list of coding agents](https://github.com/vercel-labs/skills#supported-agents), including Claude Code, Codex, Cursor, and OpenCode.

In a repository, invoke the installed skill from your coding agent:

```text
/ci-secure
```

To use it without installing, ask your coding agent to run this command and follow the generated instructions:

```bash
npx skills use "https://github.com/starslingdev/skills" --skill "ci-secure"
```

## What it scans for

Each of the ten vectors is a complete path from outside your organization to compromise, and each has a documented real-world incident behind it. Every finding renders with a "what an attacker could do" scenario rather than a rule name, so you can judge the risk without reading the detector.

* **Template injection in `run:` blocks** — attacker-controlled text is interpolated into a shell script before the shell ever sees it, so a crafted branch name or issue title executes as code on the runner.
* **Fork code executed with privileges** — a workflow on an untrusted trigger checks out and runs the attacker's head ref while holding secrets and a write token, the classic "pwn request".
* **Cache poisoning from `pull_request_target`** — a job with fork code writes the shared cache, and the poisoned entry is then restored by trusted builds on your default branch.
* **Impostor or unreachable action SHAs** — an action is pinned to a commit that is not reachable from the action's own repository, which looks like the safest possible pin while resolving to code its maintainers never published.
* **Whole-context secret dumps** — `toJSON(secrets)`, `toJSON(github)`, or `toJSON(env)` written into logs or an artifact, exposing every secret at once.
* **`$GITHUB_ENV` and `$GITHUB_PATH` hijack** — attacker-influenced text is appended to those files, letting an attacker set environment variables or prepend a directory to `PATH` for every later step.
* **Write tokens on untrusted triggers** — `pull-requests: write` and similar grants handed to a workflow anyone can trigger from a fork.
* **Credentials in caches and artifacts** — a cache or artifact `path:` that sweeps in known credential files such as `.npmrc`, `.git/config`, or a cloud credentials directory.
* **Unverified remote code execution** — `curl | bash`, and fetch-and-run against a mutable ref, where whoever controls the remote host or branch controls what runs in your job.
* **Dependency install scripts in privileged jobs** — lifecycle scripts from the dependency tree executing in a job that holds secrets, so one compromised transitive package reaches them.

## Configuration hygiene checks

Alongside the vector scan, ci-secure reports eight pass/fail configuration facts. These are hygiene, not exploit chains — a failure is a weakened defense rather than a demonstrated path in:

* **`permissions:` declared** — every workflow declares `permissions`, instead of inheriting a repository default that may be read-write everything.
* **Write grants scoped to jobs** — no workflow-level `write` for any scope except `id-token`, which is [ci-score](https://docs.starsling.dev/skills/ci-score)'s check rather than this one, and no `permissions: write-all`. Write grants belong on the jobs that need them.
* **CODEOWNERS covers workflows** — a CODEOWNERS entry covers `.github/workflows/`, so workflow changes need a specific approval rather than any approval.
* **No fork code checked out on untrusted triggers** — bare untrusted triggers pass; checking out the attacker's ref is what fails.
* **No blanket `secrets: inherit`** — reusable workflows are passed secrets by name, so a called workflow's blast radius is visible in the caller.
* **Checkout credentials not persisted** — untrusted-trigger workflows set `persist-credentials: false`, keeping the token out of `.git/config` where a later step could read it.
* **Required checks cannot be skipped** — every required status check is produced by a job that always runs. GitHub counts a skipped required check as a pass, so a check only a conditional job reports can be satisfied by never running it.
* **Fork-PR approval is effective** — the repository's approval gate covers more than accounts brand new to GitHub, a setting that otherwise gates nobody real.

Two of these read repository settings over the API, so they report as unmeasured rather than as passes when `gh` is unavailable. An unmeasured check is a coverage gap, not a clean bill.

## How the three skills differ

|                         | ci-secure                                                     | ci-score                                        | ci-speedup                                                 |
| ----------------------- | ------------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------- |
| **Question**            | Can someone outside my org compromise my CI?                  | Does my CI config follow best practices?        | What actually makes my CI slow?                            |
| **Input**               | Workflow YAML, plus four checks over the `gh` API             | Your local checkout's workflow YAML and configs | Real run history sampled over the `gh` API                 |
| **Output**              | Findings with attacker scenarios, and a fix offer per finding | A score plus one ranked fix per failed check    | A measured root-cause report of the merge-gating long pole |
| **Requires**            | A checkout, Python, and PyYAML; `gh` recommended              | A full checkout, Python, and PyYAML             | The above plus an authenticated `gh` CLI                   |
| **Zero findings means** | Those ten paths are closed, not that CI is secure             | Nothing — the score is the output               | No single dominant long pole was found                     |

All three are free, open source, run on your machine, and send nothing to StarSling.

## What it will and will not do

The skill asks which findings you want fixed and dispatches one subagent per finding group. It **never commits, pushes, or opens a pull request unasked** — by default you review the working-tree diff yourself.

Zero findings is a first-class result, reported as such rather than padded with lower-severity noise.

## Resources

<Cards>
  <Card title="See ci-secure" href="https://starsling.dev/ci-secure" description="Watch the skill scan a GitHub Actions setup for attack vectors." />

  <Card title="Skill source" href="https://github.com/starslingdev/skills/tree/main/skills/ci-secure" description="Read the open-source skill and its implementation." />

  <Card title="Launch article" href="https://starsling.dev/blog/introducing-ci-secure-an-ai-coding-agent-skill-that-closes-the-critical-attack-vectors-in-your-github-actions" description="Learn why StarSling built ci-secure." />

  <Card title="Attack vector catalog" href="https://github.com/starslingdev/skills/blob/main/skills/ci-secure/references/security-patterns.md" description="Read each vector in full, with its detection logic and reference incidents." />

  <Card title="Why these ten" href="https://github.com/starslingdev/skills/blob/main/skills/ci-secure/references/why-these-ten.md" description="See the selection criteria and what was deliberately left out." />
</Cards>
