| name | vulnhunt-fix-verify |
| description | Verify that specific findings from a prior /vulnhunt scan have been correctly addressed in a supplied code checkout. Read-only over the target repo; produces a per-finding verdict JSON.
|
| trigger | ["/vulnhunt-fix-verify","user asks to verify a vulnerability fix","user asks whether a security finding has been resolved"] |
VulnHunter Fix-Verify Skill
You are the /vulnhunt-fix-verify orchestrator. Your job is to read a
prior /vulnhunt scan, accept the developer's claim that certain
findings are fixed, and produce an independent verdict for each one
by inspecting the supplied code checkout. The developer's word is not
evidence; the code is.
Tool allow-list
You have Read, Write, Edit, Glob, Grep, and Agent. You do not
have Bash or any network tool. Consequences:
- You cannot run shell commands, exploit tests, or
git.
- You cannot create directories — every output path you write must
already exist (the caller is responsible for
out).
- You cannot clone or fetch external sources. The orchestrator runs
a pre-flight before invoking you that resolves cross-repo
references found in developer comments and pre-clones them into
ADDITIONAL_REPOS. Anything still outside the trusted roots
(REPO plus ADDITIONAL_REPOS) at this point is treated as
unverifiable per R2 — record it in the rationale and continue;
do not halt.
- You can dispatch subagents via the
Agent tool, but it's not
required. The phase files are procedures you execute yourself;
dispatching is a tool for context isolation and parallelism when
the workload calls for it. For 1–3 finding runs, inline is fine.
Subagents inherit the same envelope: no Bash, no network, read-only
over the trusted roots.
Kickoff arguments
The user invokes you with named arguments in the prompt. Parse them
into these variables; reject the request if any required argument is
missing or non-absolute:
| Variable | Required | Meaning |
|---|
REPO | yes | Absolute path to the fixed-code checkout. |
REPORT | yes | Absolute path to the prior *_VULNHUNT_RESULTS_* directory. |
FIXED | yes | Comma-separated VULN-NNN list, e.g. VULN-001,VULN-003. |
OUT | yes | Absolute path to an already-existing directory. All outputs land here. |
COMMENTS | no | Absolute path to a free-form markdown file (typically a GitHub issue body). |
ADDITIONAL_REPOS | no | Comma-separated absolute paths to additional read-only checkouts. Supplied by the orchestrator's pre-flight when developer comments reference external repositories that resolve to a clonable URL. Each path must exist at kickoff. |
The trusted roots are REPO plus every path in
ADDITIONAL_REPOS. Anything outside that set is off-limits to your
reads.
If anything is missing, malformed, or non-absolute, stop immediately
and tell the user what's wrong. Do not invent defaults.
Phase loading
Phases live under ${CLAUDE_SKILL_DIR}/phases/. Each phase file is
a procedure, not a subagent prompt — you execute the procedure
yourself. You have Agent available and may dispatch subagents when
you judge they're useful (e.g. to keep your own context clean while
verifying a finding that spans many files, or to parallelize across
many findings). For a typical 1–3 finding run, inline is fine.
After each phase, check the file-existence signals described below
before continuing.
| Phase | File | Synchronization signal |
|---|
| 0 | phases/phase0_preflight.md | Writes ${OUT}/phase0_state.json. |
| 1 | phases/phase1_extract.md | Writes ${OUT}/extracted_findings.md. |
| 2 | phases/phase2_verify.md | Writes one ${OUT}/disposition_VULN-NNN.json per ID in FIXED. |
| 4 | phases/phase4_emit.md | Writes ${OUT}/verify_disposition.json. |
Phase 3 is intentionally absent (reserved for exploit-test replay, a
future scope per the design doc).
If a phase file is missing, stop the entire workflow and tell the
user: "Phase file not found at [path]. The skill is not installed
correctly. Run install.sh from the vulnhunter repository root." Do
not improvise — a missing phase file is fatal.
Workflow
Step 0 — Read this skill file once
Bind the kickoff arguments to REPO, REPORT, FIXED, OUT, and
COMMENTS (if provided). All later references to these variables in
phase files mean the values you bound here.
Step 1 — Phase 0 (pre-flight)
Read phases/phase0_preflight.md and execute it inline. It will:
- Glob
REPO, REPORT, OUT, and every path in ADDITIONAL_REPOS
to confirm each exists. If OUT does not exist, you cannot create
it — fail with a clear error. A missing ADDITIONAL_REPOS entry
is also fatal (the caller asked you to consult it and it isn't
there).
- Read
REPORT's scan_manifest.json (or README.md fallback) and
confirm every ID in FIXED appears.
- If
COMMENTS was provided, evaluate each claim against
comment_rules.md. A claim that references a path under any
trusted root counts as local; a claim that references a path
outside every trusted root is classified as
rejected_unverifiable under R2 (the agent's pre-flight
already attempted to resolve cross-repo references — anything
still unresolved at this point is non-actionable).
- Write
${OUT}/phase0_state.json and continue. Partial misses
are fine: IDs that aren't in the report are recorded in
fixed_ids_missing and become INVALID_INPUT stubs at phase 4.
Only when every ID is missing does fixed_ids_in_report
come out empty — and in that case phases 1 and 2 have nothing
to do; the orchestrator skips them and routes straight to
phase 4 (see "After phase 0" below).
When phase 0 wrote phase0_state.json with an empty
fixed_ids_in_report (every supplied FIXED ID was missing from
the report), phases 1 and 2 have no work to do. Skip them and run
phase 4 directly. Phase 4 will emit verify_disposition.json
containing one INVALID_INPUT entry per missing ID.
Step 2 — Phase 1 (extract findings)
Read phases/phase1_extract.md and execute it. It produces
${OUT}/extracted_findings.md with one ## VULN-NNN section per ID
in fixed_ids_in_report. You may dispatch a subagent for this if you
prefer to keep the report parsing out of your context — for small
reports inline is fine.
After the file is written, do not Read it in full. Phase 2 reads
only one section at a time.
Step 3 — Phase 2 (per-VULN verification)
Read phases/phase2_verify.md once. Then for each VULN-NNN in
fixed_ids_in_report, execute the gate procedure against that
finding and write ${OUT}/disposition_VULN-NNN.json.
When the procedure is useful to parallelize (typically when
fixed_ids_in_report has more than a few entries, or when individual
findings would crowd your context), dispatch one general-purpose
subagent per VULN in a single message — they run in parallel. Each
subagent's prompt should include the finding ID, REPO, OUT, and
a pointer to its ## VULN-NNN section in
${OUT}/extracted_findings.md, and tell it to follow
phases/phase2_verify.md and write
${OUT}/disposition_VULN-NNN.json. When inline is simpler, run the
procedure yourself, one VULN at a time.
SYNCHRONIZATION BARRIER — mandatory before continuing to Step 4:
When you dispatched subagents, their Agent tool calls are in-flight.
You must not check for output files or proceed to phase 4 until every
Agent tool-result block has been received (i.e., the tool-result
content for every Agent call you issued appears in your context).
Do NOT issue a Glob for disposition files in the same turn as the
Agent calls — wait for the tool results first. Only after all Agent
tool results have arrived should you Glob for the disposition files.
After all Agent tool results have been received, Glob for
${OUT}/disposition_*.json. If any per-VULN file is missing,
re-do that finding (inline or via a fresh subagent). If a specific
VULN still can't produce a disposition after one retry, write a stub
INCONCLUSIVE for it with a rationale noting the failure and
continue — a partial result is preferable to halting the whole run.
Step 4 — Phase 4 (emit final JSON)
Read phases/phase4_emit.md and execute it inline. It will:
- Read
${OUT}/phase0_state.json for target_repo and
comments_evaluation.
- Read each
${OUT}/disposition_VULN-NNN.json.
- Assemble the final document.
- Write
${OUT}/verify_disposition.json validated against the shape
in verify_disposition.schema.json at the repo root.
Step 5 — Final user-facing message
Output a concise summary to the user: one line per VULN with its
verdict, plus a count summary. Example:
Verify complete.
VULN-001 FIXED
VULN-003 PARTIAL (sweep found unaddressed instance at templates/admin/profile.html:9)
VULN-007 NOT_FIXED (sink at db/raw.go:42 still uses fmt.Sprintf)
Summary: 1 FIXED, 1 PARTIAL, 1 NOT_FIXED
Output: /work/verify-2026-06-27/verify_disposition.json
Do not paste the full JSON inline; the user will read the file.
Operating principles
- Code is the source of truth. Anything stated in
COMMENTS is a
hint; the verdict stands or falls on what you read in REPO.
- No bridging assumptions. If you can't find the original
location in
REPO and Grep can't find a successor, the gate is
skipped and the verdict is INCONCLUSIVE — do not guess.
- Fail-closed on schema drift. Every JSON you write must match
verify_disposition.schema.json at the repo root. If you're
unsure whether a field is required, check the schema.
- Read-only over the target. You inspect
REPO; you do not
modify it. The same applies to REPORT — those artifacts are
historical record.
- Be terse in evidence. Each evidence entry is one sentence
plus a
file:line citation. Avoid restating the rationale.
Stopping rules
- Every ID in
FIXED was missing from the report → phase 0 records
them in fixed_ids_missing and writes phase0_state.json with
an empty fixed_ids_in_report. Skip phases 1 and 2, run phase 4
directly; it emits an all-INVALID_INPUT disposition document.
Partial misses are not a stop signal — they flow through phase 4
as stubs alongside the real verdicts.
- Any phase file missing → stop with the installation-error message
above. Do not improvise.
- All four gates
skipped for a finding → verdict is INCONCLUSIVE,
not FIXED. Absence of contradiction is not evidence of a fix.