name: hawkscan-ci version: 1.14.5 description: > Use when the user wants to configure HawkScan in their CI/CD pipeline — triggers on "set up hawkscan in CI", "add stackhawk to my pipeline", "scan in CI", "configure github actions / gitlab / jenkins / circleci for hawkscan", "wire hawkscan into ci/cd", or any provider-named variant. Provider-agnostic: detects the CI system from repo files, edits the pipeline file in place to add a HawkScan job, prompts the user to set HAWK_API_KEY in their CI's native secrets engine (or an organizationally-approved external secrets manager), and wires commit-SHA + branch traceability. Defers every local-scan concern (stackhawk.yml, auth, findings, triage) to the hawkscan skill — requires a working local scan path before activating. Explicit trigger only; no autonomous code-change hook.
HawkScan CI Skill
This skill graduates a working local HawkScan setup into a CI/CD
pipeline. It is a wrapper around the local-scan path the hawkscan
skill owns — it never generates stackhawk.yml, never picks auth
recipes, never parses findings. Its scope is the pipeline file alone.
Hand-off rule: if stackhawk.yml doesn't exist or doesn't validate,
the agent must invoke the hawkscan skill first. This skill does not
onboard apps from scratch.
Step 0: Hand-off Check
Before doing anything CI-specific, confirm the local scan path is in place. Run:
test -f stackhawk.yml || echo "MISSING_CONFIG"
timeout 30 hawk validate config stackhawk.yml 2>&1 | head -20
If stackhawk.yml is missing, validation fails, or applicationId is
still a placeholder (${APP_ID} with no real value, or literal
your-app-id):
"I need a working local scan first. Run the hawkscan skill (or invoke it explicitly) to generate and validate
stackhawk.yml, then come back here."
Stop. Do not proceed.
Step 1: Detect the CI Provider
Glob the repo for known CI config locations:
ls .github/workflows/*.yml .github/workflows/*.yaml 2>/dev/null
ls .gitlab-ci.yml 2>/dev/null
ls Jenkinsfile* 2>/dev/null
ls .circleci/config.yml 2>/dev/null
ls azure-pipelines*.yml 2>/dev/null
ls bitbucket-pipelines.yml 2>/dev/null
ls .buildkite/pipeline.yml 2>/dev/null
ls .travis.yml 2>/dev/null
ls -d bamboo-specs 2>/dev/null
ls concourse/*.yml 2>/dev/null
ls -d harness 2>/dev/null
ls appspec.yml buildspec.yml 2>/dev/null
| Match | Provider |
|---|---|
.github/workflows/*.yml, *.yaml |
GitHub Actions |
.gitlab-ci.yml |
GitLab CI/CD |
Jenkinsfile, Jenkinsfile.* |
Jenkins |
.circleci/config.yml |
CircleCI |
azure-pipelines*.yml |
Azure Pipelines |
bitbucket-pipelines.yml |
Bitbucket Pipelines |
.buildkite/pipeline.yml |
Buildkite |
.travis.yml |
Travis CI |
bamboo-specs/ |
Bamboo |
appspec.yml, buildspec.yml |
AWS CodeBuild / CodeDeploy |
concourse/*.yml (heuristic — confirm with user) |
Concourse |
harness/ |
Harness |
- One match → use it.
- Multiple matches → ask: "I see CI config for both X and Y in this repo. Which one should HawkScan run from?"
- No matches → ask: "I don't see any CI config in this repo. Which provider do you want to add HawkScan to? (GitHub Actions, GitLab, Jenkins, CircleCI, ...)"
Step 2: Plan the Integration
Three planning questions. Use the defaults if the user just says "sounds good".
2a. Trigger — when should the scan run?
"On pull requests, on pushes to main, on a daily schedule, or all three? Default: PRs + push to main."
2b. Blocking behavior — what happens on findings?
"Should findings above the configured threshold fail the pipeline (exit code 42 → build failure), or warn-only (log the result, exit 0)? Default: fail the pipeline."
2c. Job placement — where does the scan fit?
"Standalone parallel job (scans an ephemeral env or already-deployed preview), or chained after build (starts the app in the same job and scans it)? Default: chained-after-build."
These answers parameterize Step 5.
Step 3: Establish the API Key Secret
The skill never sets the secret itself. Prompt the user and tell them exactly where to put it.
Ask once:
"Where should
HAWK_API_KEYcome from at runtime? Most teams use their CI provider's built-in secret store (recommended). If your org requires an external secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, 1Password, Doppler), tell me which one and I'll generate the workflow to fetch from it instead."
Path A — CI-native secret store (default)
Print the exact command/UI step the user needs to run:
| Provider | Command / UI |
|---|---|
| GitHub Actions | gh secret set HAWK_API_KEY (paste interactively), or Settings → Secrets and variables → Actions |
| GitLab CI/CD | glab variable set HAWK_API_ID <id> --masked --protected AND glab variable set HAWK_API_SECRET <secret> --masked --protected — mandatory key split (GitLab's masker can't handle the . separators in hawk.<id>.<secret>). Reassemble in the job: API_KEY="hawk.${HAWK_API_ID}.${HAWK_API_SECRET}" |
| Jenkins | Credentials Store → Add credential → "Secret text", ID HAWK_API_KEY (UI only) |
| CircleCI | circleci context store-secret <context-name> HAWK_API_KEY (org-scoped Contexts), or Project Settings → Environment Variables (UI) |
| Azure Pipelines | az pipelines variable create --name HAWK_API_KEY --secret true, or variable group |
| Bitbucket | Repository Settings → Repository variables → add HAWK_API_KEY, check "Secured" |
| Others | See references/execution-shapes.md |
Path B — External secrets manager
If the user picked Vault / AWS SM / GCP SM / Azure KV / 1Password / Doppler / etc., the workflow you write in Step 5 includes a fetch step before the scan step. Fetch patterns:
- HashiCorp Vault:
vault kv get -field=hawk_api_key secret/ci/stackhawk - AWS Secrets Manager:
aws secretsmanager get-secret-value --secret-id stackhawk/hawk-api-key --query SecretString --output text - GCP Secret Manager:
gcloud secrets versions access latest --secret=hawk-api-key - Azure Key Vault:
az keyvault secret show --vault-name <vault> --name hawk-api-key --query value -o tsv - 1Password (GitHub Actions):
1password/load-secrets-action@v2withop://<vault>/<item>/<field>reference - Doppler:
doppler secrets get HAWK_API_KEY --plain
The user is responsible for the manager-side setup (storing the secret, granting the runner read access). Call those out as out-of-band actions in Step 6.
Reminder regardless of path: the secret must be set before the first pipeline run, or the scan will fail with a 401.
Step 4: Pick the Execution Shape
Three tiers, prefer highest the provider supports:
| Tier | Mechanism | When to use |
|---|---|---|
| 1. Native action | stackhawk/hawkscan-action@v2 |
GitHub Actions only |
| 2. Docker image | stackhawk/hawkscan:latest |
Anywhere with Docker (GitLab, Jenkins, CircleCI, Azure, Bitbucket, Buildkite, AWS CodeBuild) |
| 3. CLI download | download.stackhawk.com/hawk/cli/hawk-${VERSION}.zip |
Bare shell runners (Travis without sudo, some Spinnaker stages) |
Track the latest stable scanner:
- Image: use
stackhawk/hawkscan:latest— HawkScan is a security scanner, so CI should run the newest stable build for the freshest checks. - Action: pin the major (
@v2) — auto-receives the newest action within v2. - CLI: resolve the current version via
curl -s https://api.stackhawk.com/hawkscan/version(the zip is version-numbered; there's nohawk-latest.zip). - Need fully reproducible builds? Pin an explicit version (
stackhawk/hawkscan:<X.Y.Z>,@vX.Y.Z, or an image digest) instead — seereferences/execution-shapes.md.
See references/execution-shapes.md for the full per-provider mapping.
Step 5: Write or Patch the Workflow
Build the workflow block using the choices from Steps 2-4. The block must do these things, in this order:
5.1 — Checkout the repo
Use the provider's standard checkout at the commit under test.
5.2 — Build / start the app
Detect the start pattern from the repo:
ls docker-compose.yml docker-compose.yaml compose.yml compose.yaml 2>/dev/null
test -f package.json && jq -r '.scripts | keys[]' package.json 2>/dev/null | grep -E '^(start|dev|serve)$'
test -f Makefile && grep -E '^(start|run|serve):' Makefile 2>/dev/null
ls pom.xml build.gradle build.gradle.kts 2>/dev/null
- Docker Compose detected →
docker compose up -d+ wait-for-it on the health endpoint. Make sure the scanned image is built from the commit under test — build in-job (--build/ abuild:override) or build→push→pull; a stale or upstreamimage:tag gates code that isn't in this PR. Seereferences/app-startup-patterns.mdPattern 1. - npm start script →
npm install && npm start &+ wait-for-it. - Maven / Gradle →
./mvnw spring-boot:run &or./gradlew bootRun &+ wait-for-it. - None detected → ask the user for the start command.
App-startup pattern menu lives in references/app-startup-patterns.md.
5.2.5 — Confirm authenticated scans are self-contained
If the scan authenticates, the credential should come from the scan itself, not
the pipeline. HawkScan authenticates from the authentication: block in
stackhawk.yml — including app.authentication.script for multi-step
login/token-mint flows — owned by the hawkscan skill, so the same scan
authenticates identically on a laptop and in CI. Confirm that's in place rather
than replicating it in the workflow:
stackhawk.ymlalready has a validatedauthentication:block → add nothing; the scan handles auth itself.- Auth isn't configured, or the scan can't obtain the credential on its own →
route to the
hawkscanskill to add/repair the recipe, and tostackhawk-data-seedif the app needs seed data before authenticated routes return anything. - A credential that genuinely can't live inside the scan (must be seeded
out-of-band) → only then add a pre-scan step, preferring a committed
data-seed/replay over bespoke scripting.
Don't replicate a token-mint step in the workflow — it belongs in the scan's
authentication.script. Full priority order + ownership table:
references/app-startup-patterns.md Pattern 8.
5.3 — Export traceability env vars
COMMIT_SHA = provider's commit SHA expression
BRANCH_NAME = provider's branch expression
Provider expression cheat sheet:
| Provider | COMMIT_SHA |
BRANCH_NAME |
|---|---|---|
| GitHub Actions | ${{ github.sha }} |
${{ github.head_ref || github.ref_name }} |
| GitLab CI | ${CI_COMMIT_SHA} |
${CI_COMMIT_REF_NAME} |
| Jenkins | ${env.GIT_COMMIT} |
${env.BRANCH_NAME} (multibranch) |
| CircleCI | ${CIRCLE_SHA1} |
${CIRCLE_BRANCH} |
| Azure Pipelines | $(Build.SourceVersion) |
$(Build.SourceBranchName) |
| Bitbucket | ${BITBUCKET_COMMIT} |
${BITBUCKET_BRANCH} |
| Buildkite | ${BUILDKITE_COMMIT} |
${BUILDKITE_BRANCH} |
| Others | see references/execution-shapes.md |
see references/execution-shapes.md |
Do not set HAWK_AGENT. That variable attributes scans to the AI
agent that triggered the work; a CI pipeline isn't an agent. The
${HAWK_AGENT:none} default already in stackhawk.yml resolves to
none for CI runs, which is the correct attribution. Leave it alone.
5.4 — Fetch external secret (Path B only)
If the user picked an external secrets manager in Step 3, insert the
provider-appropriate fetch step before the scan step, then export the
result as HAWK_API_KEY for the scan step.
5.5 — Invoke HawkScan
The scan step itself. Pass HAWK_API_KEY from the chosen secret
source via the provider's native secret reference (e.g.,
${{ secrets.HAWK_API_KEY }} for GHA, $HAWK_API_KEY exported from
the previous step for everyone else).
5.6 — Handle the exit code
Per the Step 2b choice:
- Block: let exit code propagate. Default behavior of all the invocation shapes — the action / docker / CLI all exit non-zero on 42, the runner fails the job.
- Warn-only: downgrade only exit 42 to a warning; still fail on exit 1
(config error, app unreachable, auth failure) — never declare success
when the scan didn't run.
Shell-based runners:
GitHub Actions has a native form (set +e hawk scan HAWK_EXIT=$? case $HAWK_EXIT in 0) ;; 42) echo "::warning::HawkScan found findings — review at the platform URL above." ;; *) exit $HAWK_EXIT ;; # exit 1 = config error / unreachable / auth — never swallow esaccontinue-on-error: true). Full per-provider warn-only patterns:references/failure-semantics.md.
5.7 — Upload the scan artifact
Per provider:
| Provider | Mechanism |
|---|---|
| GitHub Actions | actions/upload-artifact@v4 |
| GitLab | artifacts: block in the job |
| Jenkins | archiveArtifacts artifacts: 'hawkscan/*' |
| CircleCI | store_artifacts step |
| Azure Pipelines | PublishBuildArtifacts@1 task |
| Bitbucket | artifacts: block |
Path: the location the invocation shape produces (typically
hawkscan-report/ for the action, or $(pwd)/hawkscan/ for Docker).
Full per-provider workflow examples live in
references/execution-shapes.md.
Pick the example matching the detected provider, parameterize per
Steps 2-4, and write the result.
Step 6: Verify and Report
After writing/patching the workflow file:
- Show the diff in chat. Don't make the user open the file to see what changed.
- List out-of-band actions the user still has to do:
- Set the secret in the provider (Path A) or in the external manager + grant runner access (Path B)
- If the scan job should be a required check, update branch protection rules in the provider UI
- Confirm
stackhawk.ymlapplicationIdandhostmatch the environment the pipeline will scan (preview URL vs ephemeral env vs local-in-runner)
- Set expectations:
"First pipeline run will be slower — HawkScan creates the App and Env in the platform if they don't exist, and the initial full scan has no prior baseline to diff against. Subsequent runs reuse the App and Env and are faster."
- Stop. Do not trigger the pipeline. Do not push the branch. Do not open the PR. The user owns the merge story.
Common Mistakes to Avoid
- Don't generate
stackhawk.yml. That's thehawkscanskill's surface. If the config is missing, hand off. - Don't set
HAWK_AGENT. CI pipelines aren't agents; the${HAWK_AGENT:none}default resolves correctly without intervention. - Use
stackhawk/hawkscan:latestfor the scanner image. It's a security scanner — CI should run the newest stable build for the best detection. Pin an explicit version only when an org reproducibility mandate requires it. - Don't inline the API key. Never, in any form. Always reference the provider's secret store or external secrets manager.
- Don't try to set the secret autonomously. Prompt the user and tell them where to put it.
- Don't forget GitLab's key-split rule.
HAWK_API_KEYin the formhawk.<id>.<secret>triggers GitLab's variable-masking bug. Split intoHAWK_API_ID+HAWK_API_SECRETand reassemble in the job. - Don't write
_STACKHAWK_GIT_*tags from this skill. Those live instackhawk.yml; if they're missing, hand off to thehawkscanskill for the edit. The CI skill only exports the env vars the tags interpolate from. - Don't scan an image that doesn't contain the commit under test. A published
image:tag built elsewhere gates code that isn't in this PR. Building in-job (--build/ abuild:override) and build→push→pull both work; scanning a stale or upstream tag is the bug. Seereferences/app-startup-patterns.mdPattern 1. - Don't hand-roll auth bootstrapping in the pipeline. The scan credential is the scan's job — it comes from
stackhawk.yml'sauthentication:block (includingauthentication.scriptfor multi-step login/token-mint flows), owned by thehawkscanskill, so it's portable local↔CI. Confirm it's configured and route tohawkscanif not; add a pipeline step only for a credential that genuinely can't live in the scan. See Pattern 8. - Don't trigger the pipeline. Editing the workflow file is the skill's full scope; running it is the user's call.