DocsGuides

GitHub Actions

Generate a pinned, attestation-verified GitHub Actions workflow with shk ci init github, or use the composite action.

On this page

shk ci init github generates a ready-to-commit GitHub Actions workflow that installs a pinned shk release (checksum + GitHub attestation verified) and scans your repository on every pull request and push to main. It is intended as a sane default that you can adopt without authoring YAML by hand, and that you can re-generate later when defaults improve.

Use --upload-sarif to publish findings to GitHub code scanning. This adds Security-tab alerts and annotations for findings on changed pull-request lines:

shk ci init github --upload-sarif
bash

The generated workflow captures the scan exit code, uploads a valid report even when findings produce exit 1, and only then applies the original exit code. Runtime/configuration failures still fail the job.

Quick Start

shk ci init github
git add .github/workflows/shk.yml
git commit -m "ci: add shk security scan"
git push
bash

This writes .github/workflows/shk.yml. The workflow:

  • Triggers on every pull_request and on push to main.
  • Installs a pinned shk release archive with SHA256 verification and gh attestation verify.
  • Runs shk scan --json --fail-on high -- . and fails the job (and the PR check) when findings reach the threshold.

With --upload-sarif, it instead runs shk scan --sarif --fail-on high -- ., grants security-events: write and actions: read, uploads with github/codeql-action/upload-sarif@v4, and then restores the scan result.

Preview without writing the file, or scan a subdirectory:

shk ci init github --dry-run
shk ci init github --path packages/api
bash

Composite Action

This repository also provides a composite action for adding shk to an existing workflow. The caller must check out the repository and grant the token permission to upload code-scanning results. The action supports Linux and macOS runners:

permissions:
  contents: read
  security-events: write

steps:
  - uses: actions/checkout@v7
    with:
      persist-credentials: false

  - uses: Kazuki-tam/security-harness-kit@v1
    with:
      path: .
      fail-on: high
      mode: blocking
      mcp-audit: true
      upload-sarif: true
yaml

Supported inputs are path, fail-on, mode (blocking or audit), mcp-audit, upload-sarif, category, and shk-version. The action exposes sarif-file, exit-code, mcp-sarif-file, and mcp-exit-code outputs. Set upload-sarif: false to generate SARIF and expose its runner-local paths without calling GitHub code scanning.

Public repositories can use code scanning without a paid license. Private and internal repositories require GitHub Code Security to be enabled. The composite action cannot grant permissions to its caller, so security-events: write must remain in the workflow when upload is enabled. Pull requests from forks normally receive a read-only token. If SARIF upload is not available for fork-triggered runs, set upload-sarif: false for those runs; do not switch to pull_request_target merely to obtain a write token while scanning untrusted pull-request code.

What The Generated Workflow Contains

# Generated by shk. Regenerate with `shk ci init github --force`; manual edits will be overwritten.
name: Security Harness Kit

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: shk-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false

      - name: Install shk
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          SHK_VERSION=v0.7.0
          REPO=Kazuki-tam/security-harness-kit
          mkdir -p "$HOME/.cargo/bin"
          case "$(uname -s)-$(uname -m)" in
            Linux-x86_64) TARGET=x86_64-unknown-linux-gnu ;;
            Linux-aarch64|Linux-arm64) TARGET=aarch64-unknown-linux-gnu ;;
            Darwin-x86_64) TARGET=x86_64-apple-darwin ;;
            Darwin-arm64) TARGET=aarch64-apple-darwin ;;
            *) echo "unsupported runner: $(uname -s)-$(uname -m)" >&2; exit 1 ;;
          esac
          ASSET="shk-cli-${TARGET}.tar.xz"
          gh release download "$SHK_VERSION" -R "$REPO" -p "$ASSET" -p "${ASSET}.sha256"
          sha256sum -c "${ASSET}.sha256"
          gh attestation verify "$ASSET" -R "$REPO"
          TMP="$(mktemp -d)"
          tar -xJf "$ASSET" -C "$TMP"
          install -m 755 "$TMP/shk-cli-${TARGET}/shk" "$HOME/.cargo/bin/shk"
          echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"

      - name: Run shk
        shell: bash
        run: |
          shk scan --json --fail-on high -- .
yaml

Why these defaults

Block Why it is there
permissions: contents: read The workflow only needs to read repository content. Explicitly minimising the GITHUB_TOKEN scopes prevents an accidental write at the workflow or organisation default level.
concurrency: cancel-in-progress: true Successive pushes to the same PR cancel the in-flight job. Cuts CI cost on busy branches without losing the result for the latest commit.
actions/checkout@v7 GitHub-owned checkout action pinned to the current major for maintainability.
persist-credentials: false Stops actions/checkout from leaving the GitHub token in a Git credential file that later steps could read. shk doctor workflows flags this for any checkout step.
Pinned release + checksum + attestation Avoids curl | sh and latest. Downloads the release archive, verifies SHA256, and checks GitHub artifact attestation before install.
--json --fail-on high JSON output is greppable / archivable from the run log. high matches the default [thresholds].scan_fail_on shipped by shk init.

When --upload-sarif is enabled, the JSON command is replaced with --sarif, and the workflow uploads the report before it applies the scanner's exit code.

Audit MCP Configurations In CI

shk scan looks at file contents, so it does not evaluate how MCP servers are configured. Set mcp-audit: true on the composite action to also run shk mcp audit against the checked-out project:

  - uses: Kazuki-tam/security-harness-kit@v1
    with:
      path: .
      fail-on: high
      mcp-audit: true
      upload-sarif: true
yaml

This catches configuration-level risks that content scanning cannot see, such as npx -y auto installs, unpinned packages, filesystem servers scoped to / or $HOME, plaintext http:// endpoints, and secrets embedded in server URLs. The audit is static: it never starts a server, resolves a command, expands a variable, or makes a network request.

Details:

  • Only project-local configuration files are audited. The action never passes --global, since a runner's home directory is not the developer's machine.
  • The audit runs after the scan and shares the fail-on input. Its SARIF report is uploaded under <category>-mcp so code scanning keeps it separate from scan results.
  • The job exit code combines both commands: 2 if either reports a runtime or configuration error, otherwise 1 if either meets the threshold, otherwise 0.
  • mode: audit also relaxes the MCP audit to non-blocking. shk mcp audit has no --audit flag of its own, so the action maps its exit 1 to 0 for you.

The workflow generated by shk ci init github runs shk scan only. To audit MCP configurations there, add a step that calls the composite action with mcp-audit: true, or run shk mcp audit --sarif -- . directly.

Choose A Mode

shk ci init github --mode <mode> selects the rollout posture.

--mode blocking (default)

The workflow exits non-zero when any finding meets --fail-on, which fails the PR check. Use this once you trust the noise level.

shk ci init github --mode blocking --fail-on high
bash

To start strict, use critical first and tighten over time:

shk ci init github --fail-on critical
bash

--mode audit

The workflow runs shk scan --json --audit -- . and always exits 0. Findings appear in the run log so reviewers can see them, but the PR check stays green. Use this for a soak period before flipping to blocking.

shk ci init github --mode audit
bash

--fail-on has no effect under --mode audit; the CLI prints a warning if you supply both.

Pin A Release

Generated workflows default to the shk version that produced them (v + crate version). Override when pointing CI at a different tag:

shk ci init github --shk-version v0.7.0
bash

Re-run shk ci init github --shk-version <new tag> --force after upgrading shk locally to refresh the pin.

--shk-version accepts either latest or a SemVer-ish tag (v?MAJOR.MINOR.PATCH[-pre]). Other values are rejected before anything is written, so they cannot reach the install script. A tag without the v prefix is normalised to vMAJOR.MINOR.PATCH.

Use A Fork Or Mirror

shk ci init github --repo your-org/security-harness-kit
bash

--repo is validated as owner/repository (alphanumerics, ., _, -). Combine with --shk-version to point CI at your fork's release tag.

Make shk A Required PR Check

Once you have a successful run on main:

  1. Open repository Settings → Branches → Branch protection rules.
  2. Edit (or add) the rule for main.
  3. Under Require status checks to pass before merging, enable the rule and pick scan (the job name from the generated workflow) as a required check.
  4. Optionally enable Require branches to be up to date before merging.

If you renamed the job or split the workflow, use the actual job name shown in the Actions tab.

Re-Generation And Manual Edits

The workflow header explicitly states that re-running shk ci init github --force will overwrite the file. Treat the file as generated:

  • For one-off tweaks (job name, runner image), it is fine to edit by hand and live with the next regeneration overwriting them.

  • For durable customisations, prefer flag-driven regeneration (e.g. pin the version, change --output to a separate workflow file) so the next shk upgrade keeps your changes.

  • To regenerate without overwriting, change --output to a different filename:

    shk ci init github --output .github/workflows/shk-next.yml
    bash

Combine With Existing Workflows

shk ci init github --output lets you co-exist with existing workflows. Example: keep the generated job in its own file rather than merging into your main test workflow:

shk ci init github --output .github/workflows/security.yml --force
bash

If you prefer to inline shk into an existing job, copy the Install shk and Run shk steps from a --dry-run invocation into the target workflow. Make sure permissions: contents: read (or stricter) is set at the workflow or job level so the surrounding workflow does not grant unintended write scopes.

Caching The Binary (Optional)

The install step downloads a single release archive, so a cache is usually unnecessary. If you want to skip the network round-trip on cache hits, wrap the install step with actions/cache keyed on --shk-version:

      - uses: actions/cache@v4
        id: shk-cache
        with:
          path: ~/.cargo/bin/shk
          key: shk-${{ runner.os }}-v0.7.0

      - name: Install shk
        if: steps.shk-cache.outputs.cache-hit != 'true'
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          # copy the Install shk block from `shk ci init github --dry-run`
yaml

Cache only when you have pinned --shk-version; caching latest defeats the purpose.

Troubleshooting

Symptom Likely cause / fix
error: invalid value '<x>' for '--fail-on <SEVERITY>' clap rejected an unknown severity. Use one of info, low, medium, high, critical.
invalid --shk-version The version is neither latest nor a v?MAJOR.MINOR.PATCH[-pre] tag. Pin to a published release tag.
<file> already exists (use --force to overwrite) The destination workflow exists. Re-run with --force, or use --output to write a new file.
shk scan exits 1 in CI but not locally Check [thresholds] in shk.toml (CI uses the same policy) and any [[allowlist]] entries. The CI command prints JSON; inspect the findings array in the run log.
Job runs but never blocks You generated --mode audit. Re-generate with --mode blocking --force to enforce.
Job fails but the scan SARIF is clean With mcp-audit: true, the MCP audit can fail the job on its own. Check the Run shk mcp audit step output and the <category>-mcp code-scanning results.