Skip to content

feat(harness): allow per-run model override via VULNHUNT_HARNESS_MODEL - #34

Open
liamthuynh wants to merge 3 commits into
capitalone:mainfrom
liamthuynh:feat/harness-model-env-override
Open

liamthuynh wants to merge 3 commits into
capitalone:mainfrom
liamthuynh:feat/harness-model-env-override

Conversation

@liamthuynh

@liamthuynh liamthuynh commented Sep 5, 2026 •

Copy link
Copy Markdown

While setting up a batch scan I hit the fact that the harness hardcodes the
scanning/judging model in local_harness/config.py, so trying a different model
for one batch or benchmark run means editing tracked source, easy to commit by
accident, and awkward when two runs need different models.

This reads it from VULNHUNT_HARNESS_MODEL instead, keeping claude-opus-4-8
as the default.

Naming. Scoped to the harness the way VULNHUNT_AGENT_CONFIG is scoped to
the agent, so it stays clear of the agent's VULNHUNT_<SECTION>_<KEY> namespace.
Reusing that component's VULNHUNT_ANTHROPIC_MODEL would not work anyway: per
the agent README it may hold a Bedrock inference-profile ID
(us.anthropic.claude-...), which the claude --model flag the harness shells
out to does not accept.

Blank values. Treated as unset. os.environ.get() falls back only on an
absent key, so an exported-but-empty variable would otherwise have produced
claude --model "" in scan.py and broken the existing non-empty assertion in
test_config.py.

Test state. The new tests restore module state through a fixture rather than
a finally inside the test body. monkeypatch undoes itself in fixture
teardown, which runs after the body, so an in-body restoring reload re-reads
the override and leaves it baked into config.MODEL for every later test.

Docs. The README now names the two traps a downgraded run walks into:
/vulnhunt stops on non-Opus models at Step 0, and scan artifacts carry no model
tag, so a cheap trial run is silently reused by a later full run on both resume
paths (--force-rescan or a fresh clone are needed to actually re-scan).

Validation

  • pytest --cov=local_harness → 169 passed (was 165), coverage steady at 95%.
  • Suite re-run with VULNHUNT_HARNESS_MODEL unset, empty, whitespace-only, and
    set to a real model id.

The scanning/judging model was a hardcoded constant, so trying a different
model for one batch or benchmark run meant editing tracked source — easy to
commit by accident, and awkward when two runs need different models.

Read it from VULNHUNT_HARNESS_MODEL instead, keeping claude-opus-4-8 as the
default. The name is scoped to the harness the way VULNHUNT_AGENT_CONFIG is
scoped to the agent, so it stays clear of the agent's VULNHUNT_<SECTION>_<KEY>
namespace. Reusing that component's VULNHUNT_ANTHROPIC_MODEL would not work
anyway: it may hold a Bedrock inference-profile ID, which the `claude --model`
flag the harness shells out to does not accept.

Blank and whitespace-only values are treated as unset. os.environ.get() falls
back only on an absent key, so an exported-but-empty variable would otherwise
have produced `claude --model ""` in scan.py and broken the existing non-empty
assertion in test_config.py.

The tests restore module state through a fixture rather than a `finally` inside
the test body. monkeypatch undoes itself in fixture teardown, which runs after
the body, so an in-body restoring reload re-reads the override and leaves it
baked into config.MODEL for every later test.

The README now names the two traps a downgraded run walks into: /vulnhunt stops
on non-Opus models at Step 0, and scan artifacts carry no model tag, so a cheap
trial run is silently reused by a later full run on both resume paths.

Validated: harness suite 169 passed, coverage steady at 95%; re-run with the
variable unset, empty, whitespace-only, and set to a real model id.
@liamthuynh
liamthuynh requested a review from a team as a code owner September 5, 2026 21:18
@CLAassistant

CLAassistant commented Sep 5, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@schenksj schenksj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @liamthuynh, this is a great contribution! Being able to override the model for one run without editing config.py is really useful. The details are handled well: blank values fall back to the default, the value is stripped, the comment explains why this doesn't reuse the agent's VULNHUNT_<SECTION>_<KEY> namespace, and the test fixture restores the environment so the override can't carry into other tests.

I'm requesting changes for one issue. With an env var, switching models is now easy, which makes an existing labeling problem in the benchmark easy to hit.

The benchmark report can name the wrong model. benchmark/run.py:427 runs state["model"] = MODEL on every run, before any phase executes. phase_scan skips any target that is already marked scanned or has valid results, and tally.py:102 writes state["model"] into BENCHMARK_REPORT.md. So:

  1. VULNHUNT_HARNESS_MODEL=<cheap-model> python -m local_harness.benchmark.run (trial run)
  2. python -m local_harness.benchmark.run (no env var, no --force-rescan)

Step 2 rescans nothing. It re-judges and tallies the trial's findings, and the report says they came from claude-opus-4-8. --tally-only mislabels results the same way.

That line is older than this PR. Before, though, changing the model meant editing a source file. Now it's one environment variable, so this PR is a good place to fix it. Either of these would work:

  • only set state["model"] when a scan actually runs, or
  • record the model on each target when it is scanned, and have the tally report the model(s) that actually produced the results (or flag a mix).

After that, please update the README caveat to match (inline comment below).

Otherwise this looks ready. The README commands and flags (--force-rescan, --re-clone) match the CLI, and the config change itself is correct. Thanks again!

Comment thread harness/README.md Outdated
Comment on lines +33 to +34
- Scan artifacts are not tagged with the model that produced them, and both
resume paths skip any target that already has results. A cheap trial run will

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling out the resume behaviour here is helpful, but "not tagged with the model that produced them" isn't quite accurate. state.json and BENCHMARK_REPORT.md are tagged with a model: whichever one was set on the most recent run (benchmark/run.py:427), not the one that produced the results. That's worse than no tag, because the report confidently names the wrong model.

Once the labeling is fixed (see the review summary), could you update this bullet to describe the new behaviour? If you decide to leave it unfixed, please state plainly that the report shows the current run's model even when the results were reused.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The model label now updates only when scans run, so reused results keep their saved label. I updated the README bullet to reflect this.

# `claude --model ""`. Read once at import, so it must be set before the process
# starts.
DEFAULT_MODEL = "claude-opus-4-8"
MODEL = os.environ.get("VULNHUNT_HARNESS_MODEL", "").strip() or DEFAULT_MODEL

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The override logic looks good, and stripping the value plus falling back on blank is the right call.

One thing to fix with it: in the benchmark, MODEL is also used as a label, not just passed to claude --model. benchmark/run.py:427 copies it into state["model"] even when every target is skipped, so after this change a run without the env var will relabel results from an earlier override run. Details are in the review summary.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. state["model"] now updates only when scans run. Resumed runs and --tally-only keep the saved label, with regression tests covering both.

Comment thread harness/tests/test_config.py
@liamthuynh
liamthuynh requested a review from schenksj October 6, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants