git:20260914.7edcfdc to git:20260914.3648225
8 added, 34 removed. Audit A to A.
---
description: >
- HawkScan scan-policy reference: first scan is one broad detected-stack policy run to completion; broadest scan runs last because the last completed scan is the result; follow-up full scans prune tech flags or fix auth/spec instead of raising strength; a named org policy attaches only via app.scanPolicy.name; hand-built policy traps (STRENGTH_LOW/THRESHOLD_LOW dropped as protobuf zero values, stripping pluginType drops passive rules); why optimize Setup runs on every fresh stackhawk.yml.
+ HawkScan scan-policy reference: first scan is one broad detected-stack policy run to completion; broadest scan runs last because the last completed scan is the result; follow-up full scans prune tech flags or fix auth/spec instead of raising strength; a named org policy attaches only via app.scanPolicy.name; hand-built policy traps (STRENGTH_LOW/THRESHOLD_LOW dropped as protobuf zero values, stripping pluginType drops passive rules); set tech flags to the detected stack before the first scan.
globs:
- "**/stackhawk.yml"
- "**/stackhawk-*.yml"
alwaysApply: false
---
# Scan Policy Reference
How the scan policy is chosen, attached, and tuned across the first scan and any follow-up
full scans. Read this before Phase 0c and before starting a second full scan. Field syntax is
canonical in `hawk config show app.scanPolicy --text`; the optimize skill owns policy creation.
## Contents
- [First scan: one broad policy, run to completion](#first-scan-one-broad-policy-run-to-completion)
- [Tech flags before the first scan](#tech-flags-before-the-first-scan)
- [Follow-up full scans: prune, do not crank](#follow-up-full-scans-prune-do-not-crank)
- [Attaching a named org policy](#attaching-a-named-org-policy)
- [Hand-built policy traps](#hand-built-policy-traps)
- - [Why Phase 0c runs on every fresh config](#why-phase-0c-runs-on-every-fresh-config)
---
## First scan: one broad policy, run to completion
- **One broad policy for the detected stack.** The first scan uses the preset that matches the
app shape (`DEFAULT`, `DEFAULT_API`, or the GraphQL preset — confirm names with
`hawk op policy list`) plus the detected tech flags, built by optimize Setup (Phase 0c). Not
every plugin at HIGH strength; not a hand-picked subset.
- **Run it to completion.** Do not stop it early and do not chain several long scans. The
first completed broad scan reaches most of the findings any config can reach; further hours
add a finding or two, and a follow-up at lowest threshold / HIGH strength adds time, not findings.
- **Broadest scan last.** When several full scans run in one env, the **last completed scan is
- the result** — on the platform and for anyone grading the output. Ending on a narrow scan
+ the result** on the platform. Ending on a narrow scan
(`includePaths` on a few routes, a reduced plugin set) replaces the broad result. Narrow
diagnostic scans are fine mid-session; finish with the broad one.
## Tech flags before the first scan
- A newly created app has **every** tech flag enabled. Left that way, the scan runs rules for
- databases, languages, and frameworks the app does not have, which roughly doubles scan time
- with no recall gain. Phase 0c via optimize Setup is the normal route: it writes the detected
- flags into the named policy. When Phase 0c is skipped, or optimize degrades to recommend-only,
- set the app's flags directly before the first scan:
-
- 1. Detect the stack from the source — manifests and evidence files (`package.json`, `pom.xml`,
- `go.mod`, `requirements.txt`, `Gemfile`, `*.csproj`, `docker-compose.yml`). Heuristics and
- flag names are in the tech-flags reference linked from SKILL.md Phase 0c.
- 2. Fetch the canonical flag list; only keys it returns are valid:
- ```bash
- hawk op app tech-flags get --app <NAME|UUID> --format json
- ```
- 3. Disable all, then enable only the detected flags (enable a parent with its child, e.g.
- `Language.Java` with `Language.Java.Spring`):
- ```bash
- hawk op app tech-flags disable-all --app <NAME|UUID> --yes
- hawk op app tech-flags set --app <NAME|UUID> <Flag.Key>=true <Other.Flag.Key>=true
- ```
-
- If detection finds no evidence, leave the flags alone: a wrong "off" hides findings, a wrong
- "on" only costs time.
+ A newly created app has **every** tech flag enabled, so the scan runs rules for databases,
+ languages, and frameworks the app does not have and takes longer with no coverage gain. Before
+ the first scan, set the flags to the detected stack: optimize Setup does this in Phase 0c, and
+ SKILL.md Phase 0c gives the fallback when optimize is skipped or degrades to recommend-only.
+ Detection heuristics and flag names are in tech-flags.md; the `hawk op app tech-flags` commands
+ are in platform-model.md. If detection finds no evidence, leave the flags alone.
## Follow-up full scans: prune, do not crank
Terminology: `hawk rescan --scan-id` is fix verification (SKILL.md Step 3) and is not what this
section covers. This is about a *second full `hawk scan`* after the first broad one.
Run a follow-up full scan only for a reason the quality gate named (SKILL.md Step 4.5 — spec
not wired, auth wall, base-path mismatch) or to **prune**: drop tech flags for stacks the app
does not use, exclude paths that are pure noise. Do **not** raise strength or lower threshold
across all plugins — it multiplies scan time and adds no findings. To change
the policy, re-run optimize Setup or edit the named policy through `hawk op policy`; do not
hand-edit policy JSON from memory (see traps below).
## Attaching a named org policy
A named org policy reaches a `stackhawk.yml` scan only through `app.scanPolicy.name`:
```yaml
app:
scanPolicy:
name: <POLICY_NAME> # e.g. OPTIMIZE_TRIAL_MYAPP_DEVELOPMENT — must match ^[A-Z0-9_]+$
# excludePluginIds: [] # optional local toggles layered on top of the named policy
# includePluginIds: []
```
Creating a policy on the platform, or assigning it as the app default, does not by itself
change what a config that names a *different* policy scans with. Confirm the field shape with
`hawk config show app.scanPolicy --text` and that the policy exists with
`hawk op policy list --format json`. If the scan log shows a different policy than the one you
named, the name did not match — names are exact and upper-case.
## Hand-built policy traps
Prefer optimize Setup (`hawk op policy get --name <PRESET>` → edit → `hawk op policy create
--file`). If you must hand-edit policy JSON, start from a `policy get` dump and change as
little as possible. Two traps:
| Trap | Symptom | Rule |
|------|---------|------|
| **Protobuf zero values are dropped.** `STRENGTH_LOW` and `THRESHOLD_LOW` are the zero values of their enums, so a policy that sets them is serialized without those fields and the plugin entry lands with no strength/threshold. | A ~30-second scan, 0 findings, from a policy that "looked right". | Never write `STRENGTH_LOW` / `THRESHOLD_LOW` explicitly. Keep each plugin's strength/threshold exactly as the preset dump has them; never delete or blank them. The same mechanism applies to `enabled`: only `true` is stored, an omitted `enabled` means disabled. |
| **Stripping `pluginType` drops the passive rules.** Only passive entries carry `pluginType: PASSIVE`; active entries omit the field because `ACTIVE` is the protobuf zero value. A hand-built JSON that drops `pluginType` from the passive entries loses every passive rule — headers, cookies, information disclosure. | The scan completes, but there are zero passive findings (no missing-header, CSP, or cookie-flag alerts). | Keep every field from the `policy get` dump on every entry. Only toggle `enabled` or add and remove whole entries. |
Before `hawk op policy create`, diff the JSON you are about to submit against the preset dump
(`hawk op policy get --name <PRESET> --format json`): same plugin count, the same set of
`PASSIVE`-tagged entries as the preset dump, no `STRENGTH_LOW`/`THRESHOLD_LOW`. Check *before* creating — on current hawk,
`policy get` reads presets reliably but may not read back an org policy.
-
- ## Why Phase 0c runs on every fresh config
-
- SKILL.md Phase 0c runs optimize Setup whenever a `stackhawk.yml` is **created** — not only the
- first time an *application* is onboarded. A reused app with a fresh config (new clone, new env,
- new operator) otherwise gets no policy step at all, and the agent hand-builds a policy and
- hits a trap above. Setup is
- non-destructive (a trial policy referenced by `app.scanPolicy.name`; the app's own flags are
- untouched), so re-running it on a reused app is safe.