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-sarifThe 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 pushThis writes .github/workflows/shk.yml. The workflow:
- Triggers on every
pull_requestand onpushtomain. - Installs a pinned
shkrelease archive with SHA256 verification andgh 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/apiComposite 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: trueSupported 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 -- .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: trueThis 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-oninput. Its SARIF report is uploaded under<category>-mcpso code scanning keeps it separate from scan results. - The job exit code combines both commands:
2if either reports a runtime or configuration error, otherwise1if either meets the threshold, otherwise0. mode: auditalso relaxes the MCP audit to non-blocking.shk mcp audithas no--auditflag of its own, so the action maps its exit1to0for 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 highTo start strict, use critical first and tighten over time:
shk ci init github --fail-on critical--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--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.0Re-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--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:
- Open repository Settings → Branches → Branch protection rules.
- Edit (or add) the rule for
main. - 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. - 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
--outputto a separate workflow file) so the nextshkupgrade keeps your changes. -
To regenerate without overwriting, change
--outputto a different filename:shk ci init github --output .github/workflows/shk-next.yml
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 --forceIf 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`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. |
Related
shk ci init githubreference incommands.md.shk hooks installfor the local Git pre-commit equivalent.shk scanfor the underlying scan command and exit code semantics.shk mcp auditfor the MCP configuration rules and exit codes.