name: hawkscan version: 1.14.5 description: > Runs the HawkScan DAST security loop — configure, scan, fix all reported vulnerabilities (not just your changes), rescan to verify. Use when the user asks to run or perform a security/DAST scan, to test an app or API for vulnerabilities, or to verify a vulnerability is fixed; and AUTONOMOUSLY right after you complete a code change (feature, bugfix, refactor) — "done" means "done and secure," so run the loop without asking permission. Do NOT trigger for: informational questions about what HawkScan is, detects, or how it works (e.g. "what vulnerabilities does HawkScan find?"); editing stackhawk.yml or other config without running a scan; querying existing findings, security posture, untriaged counts, or scan history (use the stackhawk-api skill); documentation-only changes; installing or setting up the CLI; or when the user explicitly says to skip scanning.
HawkScan Skill
This skill enables Claude to act as the security testing orchestrator in an agentic coding loop. The core workflow is:
Code changes → Start Application/API → Configure HawkScan → Run scan → Parse findings → Generate fix tasks → Repeat
Prerequisites (preflight — run before anything else)
This skill requires hawk v5.5.11 or newer. Verify:
hawk version
hawk config --help >/dev/null 2>&1 || echo "MISSING: hawk config — upgrade hawk to v5.5.11+"
If hawk is older than 5.5.11 or hawk config --help fails, stop and tell the user to
upgrade before proceeding. Do not inline auth recipes from memory — they live in
hawk config show and are stale by design when hardcoded.
See references/installation.md for upgrade and install instructions.
Companion Skills
The api skill wraps read-only StackHawk platform lookups via the hawkop CLI:
| Purpose | Command |
|---|---|
| Check if App exists | hawkop app list --format json |
| Check if Env exists | hawkop env list --app <APP_ID> --format json |
| Get findings with triage | hawkop scan get --app <NAME> --detail full --format json |
| List ASM repos | hawkop repo list --format json |
| Link app to ASM repo | hawkop repo link --repo-id <ID> --app-id <ID> |
| Get tech flags | hawkop app tech-flags get --app <NAME> --format json |
| Disable all tech flags | hawkop app tech-flags disable-all --app <NAME> --yes |
| Set specific tech flags | hawkop app tech-flags set --app <NAME> Key=true |
| Triage a finding | hawkop scan triage --scan <ID> --hash <HASH> --status false-positive --note "" |
| Bulk triage from file | hawkop scan triage --scan <ID> --from-file triage.yaml |
| Annotate w/o triage perm | hawkop finding note --scan <ID> --hash <HASH> --note "..." |
If hawkop is not installed, the api skill documents raw REST fallbacks.
The stackhawk-data-seed skill sets up checked-in backend seed data via hawk perch seed.
Hand off to it when authentication fails because the backend has no valid credential
(Phase 1c.6), or after a scan when auth succeeded but endpoints returned empty data
(Step 6). This skill then consumes its .data-seed-credentials.env handoff.
StackHawk Platform Model (read this first)
Before running a scan, understand these four layered objects:
- Organization (
orgId): the tenant. Implicit viaHAWK_API_KEY. - Application (
applicationId, UUID): long-lived; holds tech flags and metadata. One App spans many Environments. - Environment (
env, string name): scan context under an App. Findings compare scan-to-scan within the same env. - Scan: a single run. Tagged with commit SHA and branch for traceability.
Non-negotiable rules: Apps are reused, not created per scan. Envs group history — pick names deliberately. Findings have a lifecycle (NEW, FALSE_POSITIVE, RISK_ACCEPTED, ASSIGNED) — respect it.
→ Deep reference: references/platform-model.md
Phase 0: App Setup & Verification
Run Phase 0 once when onboarding a new application (stackhawk.yml being created for
the first time). Do NOT run on every scan.
Phase 0a — Repo Linking: Associate the app with its source repo in Attack Surface
Management. Get the git remote URL, normalize it (lowercase, strip .git, strip host prefix
to owner/repo), match against hawkop repo list --format json output, and run
hawkop repo link --repo-id <UUID> --app-id <UUID>. If no match, inject a git_origin tag
into stackhawk.yml.
→ Full normalization rules and SSH/HTTPS edge cases: references/repo-linking.md
Phase 0b — Agent Tagging: Add the _STACKHAWK_AGENT tag to stackhawk.yml once if missing:
tags:
- name: _STACKHAWK_AGENT
value: ${HAWK_AGENT:none}
Phase 0c — Tech Flag Detection: Detect codebase evidence (package.json, pom.xml, go.mod,
requirements.txt, Gemfile, *.csproj). If evidence found: disable all flags then enable only
detected ones. If no evidence: skip, do not touch flags.
→ Full detection heuristics and flag names: references/tech-flags.md
Step 1: Assess Context
Env Name Algorithm — used wherever an environment name must be resolved. First match wins:
STACKHAWK_ENVenv var set → use it exactlyCI=trueorGITHUB_ACTIONS=true→CI- Git branch
main/master/production→Production;staging→Staging; otherwise →Development
Step 1a: Profile the Application
Build a minimal app profile before generating config. Collect three things:
API type — detect signals:
find . -not -path "*/node_modules/*" -not -path "*/vendor/*" \( -name "openapi*.yaml" -o -name "openapi*.json" -o -name "swagger*.yaml" -o -name "swagger*.json" \) 2>/dev/null | head -5
find . -not -path "*/node_modules/*" -not -path "*/vendor/*" -name "*.proto" 2>/dev/null | head -5
find . -not -path "*/node_modules/*" -not -path "*/vendor/*" \( -name "*.graphql" -o -name "schema.graphql" \) 2>/dev/null | head -5
find . -not -path "*/bin/*" -not -path "*/obj/*" \( -name "*.csproj" -o -name "Program.cs" \) 2>/dev/null | head -5
OpenAPI/Swagger → REST + openApiConf; .proto → gRPC + grpcConf; .graphql → GraphQL + graphqlConf;
.csproj/Program.cs → ASP.NET Core REST; none → standard web app.
Auth requirement — detect signals:
grep -rn --include="*.cs" --exclude-dir=bin --exclude-dir=obj -E "\[Authorize|AddAuthentication\(|UseAuthentication\(" . 2>/dev/null | head -3
grep -rn --include="*.java" --exclude-dir=target --exclude-dir=build -E "@PreAuthorize|@Secured|class\s+SecurityConfig" . 2>/dev/null | head -3
grep -rn --include="*.js" --include="*.ts" --exclude-dir=node_modules --exclude-dir=dist -E "(require|from)\s*['\"].*?(passport|express-jwt|jsonwebtoken|@auth0)" . 2>/dev/null | head -3
Two or more independent signals (or one framework-level signal like AddAuthentication( or class SecurityConfig) → auth required → follow Phase 1c. Anti-pattern: do NOT scan unauthenticated first when auth is detected — the spider hits /login, can't proceed, and returns ~0 findings by design.
Startup pattern and host:
find . -name "launchSettings.json" -path "*/Properties/*" -exec cat {} + 2>/dev/null
find . -name "docker-compose*.yml" 2>/dev/null | head -3
jq '.scripts // {}' package.json 2>/dev/null
When file access is limited, ask the user directly for: API type, auth requirement with login endpoint, and startup command + host/port.
Step 1b: SPA Framework Detection
Check for SPA frameworks before generating config:
node -e "const p=require('./package.json'); const deps={...p.dependencies,...p.devDependencies}; const spa=['react','next','vue','@angular/core','svelte','gatsby','nuxt']; console.log(spa.filter(f=>deps[f]).join(','))" 2>/dev/null
find . -not -path "*/node_modules/*" \( -path "*/pages/api/*" -o -path "*/app/api/*" -o -path "*/server/api/*" -o -path "*/server/routes/*" -o -name "server.js" -o -name "server.ts" \) 2>/dev/null | head -5
Outcomes:
- SPA + API routes → register as two separate apps (frontend + API). See
references/spa-scanning.mdScenario B. - SPA + no API routes → enable Ajax Spider; note that the backend API is the higher-value target and ask the user whether to scan frontend, backend, or both. See
references/spa-scanning.mdScenario A/C. - No SPA → proceed normally; do not add Ajax Spider config.
Rule: Never scan a SPA app without the Ajax Spider enabled. Never wait for a low path count to add it.
→ Full strategy and config templates: references/spa-scanning.md
Step 1c: Environment Checks (run in order)
- App running? HawkScan requires a live target. Start it first if not running.
stackhawk.ymlpresent? If missing → Step 2a (generate). If present → Step 2b (tune).- Credentials? Check
~/.hawk/hawk.properties(written byhawk init). If missing: runhawk init. For CI/CD: setHAWK_API_KEYas a secret and prefix invocations withAPI_KEY=$HAWK_API_KEY hawk <cmd>. If a later command returns 401/403, re-runhawk init. - Runtime? Check
which hawk. If found: use CLI. If not: checkdocker --version. If both absent: seereferences/installation.md. - App exists? Run
hawkop app list --format json. Match by name (normalized: lowercased,_and-equivalent). Exactly one match → use itsapplicationId. Multiple matches → pick by host if established, else surface to user. No match → create:
Resolvehawk create app --name "<repo-name>" --env <env-name><env-name>with the Env Name Algorithm above. Announce the created app ID and URL. - Env exists? Determine env name via Env Name Algorithm. Run
hawkop env list --app <APP_ID> --format json. Reuse if exists; otherwise runhawkop env create --app <APP_ID> --env <name> --host <url>.
Step 2a: Generate stackhawk.yml from Scratch
Use the applicationId and env from Step 1c. Minimum viable config:
app:
applicationId: ${APP_ID}
env: ${APP_ENV:Development}
host: ${APP_HOST:http://localhost:8080}
Always use env var interpolation (${VAR:default}) for sensitive values and anything
that varies across environments. Use ${VAR:default} (single colon, not ${VAR:-default}).
The entire YAML value must be the variable — host: "https://${HOST}/api" will NOT interpolate.
Never create a separate stackhawk.local.yml for host overrides. Use
host: ${APP_HOST:https://your-default-host.com} and override at runtime:
APP_HOST=http://localhost:3000 hawk scan
After writing stackhawk.yml, always validate:
timeout 30 hawk validate config stackhawk.yml || echo "Validate timed out — ensure hawk CLI 5.5.0+ is installed"
Do not proceed to Step 3 until validation passes.
→ API-type-specific config (OpenAPI, GraphQL, gRPC, seed paths, spider tuning):
references/config-patterns.md
Phase 1c: Authentication Configuration
Use hawk config show to fetch the canonical recipe for the app's auth pattern.
Step 1 — List available auth methods:
hawk config show app.authentication --text
Step 2 — Pick one by observed app behavior:
→ Auth pattern decision table: references/auth-config.md
If no row matches → jump to Phase 1c.5. Do not force-fit a recipe or proceed without auth.
Step 3 — Fetch each relevant section: hawk config show <section> --text. Use the returned YAML example as template.
Step 4 — Always include a testPath: hawk config show app.authentication.testPath --text. The testPath must return 401/403 without auth and 200 with auth.
Step 5 — Validate before scanning (mandatory whenever authentication: is authored or modified):
hawk validate config stackhawk.yml # structural check
hawk validate auth stackhawk.yml # live auth check
→ Full recipe steps and fetch commands: references/auth-config.md
Phase 1c.5: Auth Analyzer Fallback
Invoke when: (1) auth signals exist but pattern doesn't match any Phase 1c table row;
(2) hawk validate auth returned non-zero after Phase 1c (wrong recipe, not empty datastore);
(3) user explicitly requests interactive setup.
hawk perch onboard is the wizard — it captures real HTTP traffic via Chrome, runs the
validate-auth loop with structured per-field errors, and streams JSONL phase events.
Always run hawk perch stop on every exit path (onboard does not own the daemon).
→ Full flow, event handler matrix, error table, and re-run behavior:
references/auth-analyzer-fallback.md
Phase 1c.6: Seed Backend Data (empty datastore)
Use when the recipe is correct but auth fails because the credential doesn't exist in the backend (fresh local stack, just-migrated database, empty users/api_key tables). Gate first:
hawk perch seed validate --help >/dev/null 2>&1 && hawk perch seed finalize --help >/dev/null 2>&1
If gate passes and stackhawk-data-seed is installed → invoke it. It produces
.data-seed-credentials.env which this skill then consumes. If hawk is too old → tell the
user to upgrade (brew upgrade stackhawk/cli/hawk, hawk ≥ 5.6.11) or manually create the
dev credential. For gateway/multi-service apps, the credential lives in an upstream service's
datastore — run the seed against that repo, not the gateway.
After seeding, re-run hawk validate auth stackhawk.yml and continue.
Step 2b: Tune Existing stackhawk.yml
Review the config against the current app state:
- Low path count? Check in order: SPA/JS app → enable
hawk.spider.ajax: true; API spec available → wireopenApiConf/graphqlConf; specific known-deep paths → addseedPaths. OmitseedPathsunless there is a specific reason — they're rarely needed when Ajax Spider or a spec is configured. - Auth failing? Verify
authenticationblock; re-fetch the relevant recipe viahawk config show <section> --text(Phase 1c). - Too noisy / too slow? Add
app.excludePathsorapp.includePaths; tunehawk.spider.maxDurationMinutes. - New API type added? Add corresponding
graphqlConf,openApiConf, etc. - Need custom headers? Use
hawkAddOn.replacerfor tenant or API version headers. - Running in CI? Add commit SHA tags (top-level in
stackhawk.yml, not underapp:):tags: - name: _STACKHAWK_GIT_COMMIT_SHA value: ${COMMIT_SHA:none} - name: _STACKHAWK_GIT_BRANCH value: ${BRANCH_NAME:none}
Validate after any modification:
timeout 30 hawk validate config stackhawk.yml || echo "Validate timed out — ensure hawk CLI 5.5.0+ is installed"
Step 3: Validate and Run
Pre-flight: Run scan commands synchronously — never with
&ornohup. Wait for the exit code. Do not start a new scan while one is already running for this app/env.
Set env vars, then validate:
export COMMIT_SHA=$(git rev-parse HEAD)
export BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
Export HAWK_AGENT using the platform + model detection block from
references/agent-detection.md. Skip if HAWK_AGENT is
already set (CI/CD override).
hawk validate config stackhawk.yml
hawk validate api stackhawk.yml
if grep -qE '^\s*authentication:' stackhawk.yml; then
hawk validate auth stackhawk.yml
fi
Run hawk validate config every time the config changes. Run hawk validate api when
adding or modifying OpenAPI spec references. Run hawk validate auth whenever the
authentication: block is new or modified — do not skip it. Fix all failures before scanning.
Config file path rules (common agent mistake): Validate and scan commands use positional
arguments only — no -c or --config flag. Use bare filenames (not absolute paths). See
references/cli-reference.md.
CLI Reference
→ Full command reference (flags, diagnostics, perch daemon, exit codes):
references/cli-reference.md
Quick reference for agentic scanning:
hawk scan --json-output # structured output (requires Dev Release v5.3.41+)
hawk rescan --scan-id <SCAN_ID> --json-output # fast fix verification — re-runs only fired plugins
Always rescan against the original full-scan ID. Rescan IDs are not valid parent scan references.
Exit Codes
| Code | Meaning |
|---|---|
0 |
Scan complete, no findings at or above failureThreshold |
1 |
Scan failed (config error, app unreachable, auth failure) |
42 |
Scan complete, findings met or exceeded failureThreshold |
Step 4: Parse Findings and Generate Fix Tasks
Use --json-output for structured results (requires Dev Release v5.3.41+). Suppresses all
other stdout — do not combine with --trace. Fix ALL findings the scan reports — not just
findings related to recent changes. DAST scans the running application as a whole; a
pre-existing SQL injection is just as exploitable as one introduced today.
→ Full JSON schema, field reference, fix task format, and common findings guidance:
references/findings-and-fixes.md
→ Per-finding guidance on high-iteration findings (CSP, CORS, Auth, Missing Headers) — what
"done" looks like, verify commands, escalation thresholds:
references/high-iteration-findings.md
Step 5: Filter Findings by Triage State
Filter by the per-path status field (findings[].paths[].status) before fixing:
- SKIP
FALSE_POSITIVEorRISK_ACCEPTEDpaths — a human already decided these are not actionable. If every path of a finding is SKIP, skip the finding entirely. - PRIORITIZE
ASSIGNEDpaths beforeNEWof the same severity — confirmed real. - FIX
NEWpaths in severity order: High → Medium → Low; within same severity: injection > auth bypass > IDOR > XSS > header issues.
Marking false positives — mark NEW findings that are clearly false positives before routing the rest to the fix loop:
hawkop scan triage --scan <SCAN_UUID> --hash <FINDING_HASH> --status false-positive \
--note "<reason> [triaged by ${HAWK_AGENT:-agent}]"
Always append [triaged by ${HAWK_AGENT:-agent}] to every note for platform audit trail attribution.
Rules:
- ✅ Mark
FALSE_POSITIVEautonomously with a clear reason note - ❌ Never mark
RISK_ACCEPTEDorASSIGNED— human decisions only - ❌ Do NOT suppress findings in the codebase to hide scanner results
- ⚠️ If
scan triageis denied (noWRITE_TRIAGE): fall back tohawkop finding note; tell the user aWRITE_TRIAGEholder must apply the status
After triage: "Marked [N] findings as false positive. Routing [M] remaining NEW findings to fix loop. Platform: https://app.stackhawk.com/scans/
→ Config-based suppression patterns (excludePaths, excludePlugins) and WRITE_TRIAGE fallback:
references/false-positives.md
Step 6: Determine Loop Behavior
After generating fix tasks:
- Exit code 0, no findings above threshold: Scan passed. If auth succeeded but endpoints returned empty bodies (list routes
[], detail routes 404), the backend data may be empty — suggest thestackhawk-data-seedskill (don't auto-run) and rescan. See Phase 1c.6. - Exit code 42: Hand fix tasks to the coding agent. After fixes, rescan to confirm remediation. Repeat until exit 0 or only accepted-risk findings remain.
- Exit code 1: Do NOT hand fix tasks. Diagnose first:
Common causes: app not reachable, auth failure, invalidhawk validate config stackhawk.yml hawk validate api stackhawk.ymlapplicationId, config parse error.
Autonomous Security Loop
When this skill is active AND you complete a meaningful code change, run the security loop automatically without waiting for the user to ask.
Trigger when all are true: code change just completed; app is running; hawk CLI is initialized; skill is active.
Skip for: documentation-only changes; config edits without code changes; exploratory tasks; when the user explicitly says to skip.
The loop (full 7-step detail with guard rails):
→ references/autonomous-loop.md
Guard rails summary:
- One scan at a time — never run
hawk scanorhawk rescanwith&ornohup - Max one fix-rescan cycle per task — if findings remain after fixing, report them
- Always announce: "Running security scan...", "Found N vulnerabilities, fixing...", "Rescanning..."
- Interruptible — stop immediately if the user says to
Common Mistakes to Avoid
- Don't scan before the app is running. HawkScan will exit 1 with a connection error.
- Try
https://first — HawkScan accepts self-signed certificates. Only fall back tohttp://if the scan actually fails to connect with a TLS error. - Don't hardcode API keys or credentials in
stackhawk.yml. Use env vars. - Low path count ≠ clean app. Feed an API spec or auth config before concluding the app is secure. If auth succeeds but endpoints return empty data, seed the backend (Phase 1c.6).
- Don't ignore exit code 42. It's a deliberate signal that findings crossed the threshold — treat it as a build failure.
- String interpolation mid-value doesn't work.
host: "https://${HOST}/api"will NOT interpolate. Usehost: ${FULL_HOST_URL}. - Never refer to the scanner as ZAP. The product is HawkScan / StackHawk. The underlying engine is HSTE, not ZAP. Neither "ZAP" nor "OWASP ZAP" should appear in output or instructions.