Context
github/gh-aw-threat-detection v0.4.12 (PR #918) adds three per-attempt kill switches to the threat-detect binary that we invoke from compiled detection jobs:
--engine-timeout — per-attempt wall-clock timeout (detector default: 5m). On expiry, the engine subprocess and every descendant it spawned are killed with a process-group SIGKILL. Terminal — never retried, because a runaway model is overwhelmingly likely to run away again.
--max-turns — per-attempt agentic turn cap (detector default: 50). Exported to the engine as GH_AW_MAX_TURNS and additionally passed as --max-turns to Claude's bare CLI.
--retries — from-scratch retries after a "clean exit without verdict" (detector default: 0). Timeouts and engine crashes are terminal regardless.
Each of these is also readable from an env var: THREAT_DETECTION_ENGINE_TIMEOUT, THREAT_DETECTION_MAX_TURNS (with GH_AW_MAX_TURNS as a fallback), THREAT_DETECTION_RETRIES.
Today the compiled .lock.yml invocation is just:
- name: Run threat detection
run: |
threat-detect --engine ${{ ... }} --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection
so a workflow author can't override any of these three caps per workflow. The detector's own defaults are already sane, but two use cases need per-workflow tuning:
- Heavy prompts / lots of files. A workflow that legitimately expects the detector to inspect many patches or a very large
agent_output.json may need --engine-timeout 10m or --max-turns 100.
- Fast, cheap workflows. A trigger-happy workflow that runs on every PR comment may want to tighten to
--engine-timeout 90s to cap credit spend on a burst.
Note: GH_AW_MAX_TURNS is already piped through — the standalone detector reads it as a fallback for --max-turns. So a workflow that sets engine.max-turns in frontmatter for its main agent already applies the same cap to the detector today, no compiler change needed. This issue is about the two flags that aren't covered (--engine-timeout, --retries) plus a detector-specific override for --max-turns that doesn't couple it to the main agent's cap.
Proposal
Add a threat-detection (or safe-outputs.threat-detection, wherever the existing knobs live) frontmatter section:
threat-detection:
engine-timeout: 10m # Go duration; 0 disables
max-turns: 100 # Detector-only override; when unset falls back to top-level engine.max-turns / GH_AW_MAX_TURNS, then to the detector's own default (currently 50)
retries: 1
Compiler behavior:
- Only emit each flag when the user set it in frontmatter. When a key is unset in the workflow's frontmatter, the compiler MUST NOT emit the corresponding flag (or an env var) on the shell line. The detector's own compiled-in default (
5m / 50 / 0) then applies. This is the key rule: if the compiler hardcoded the detector's current default onto every emitted line, bumping that default upstream (as we just did going from 20 → 50 on --max-turns) would require every workflow to recompile before picking it up. Emitting nothing on unset keeps the coupling one-way.
- Prefer flags over env vars in the emitted script so the invocation is self-describing.
- Validate at compile time: reject negative durations/ints (the detector already rejects them with
config_error, but catching it at compile time is a better UX).
Resulting invocation for a workflow that sets all three:
- name: Run threat detection
run: |
threat-detect \
--engine ${{ ... }} \
--engine-timeout 10m \
--max-turns 100 \
--retries 1 \
--output /tmp/gh-aw/threat-detection/detection_result.json \
/tmp/gh-aw/threat-detection
Resulting invocation for a workflow that sets none of them (unchanged from today):
- name: Run threat detection
run: |
threat-detect --engine ${{ ... }} --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection
Compatibility
- Backward compatible: workflows that don't set the new keys keep the current behavior (detector defaults apply, invocation is byte-identical to today's).
- No coordinated release needed — the detector already accepts all three flags. This is a pure code-generation change in
gh-aw.
- No
.lock.yml migration needed for workflows that don't opt in.
- Future detector-default changes propagate automatically to workflows that haven't opted into an explicit override.
Related
Context
github/gh-aw-threat-detectionv0.4.12 (PR #918) adds three per-attempt kill switches to thethreat-detectbinary that we invoke from compiled detection jobs:--engine-timeout— per-attempt wall-clock timeout (detector default:5m). On expiry, the engine subprocess and every descendant it spawned are killed with a process-groupSIGKILL. Terminal — never retried, because a runaway model is overwhelmingly likely to run away again.--max-turns— per-attempt agentic turn cap (detector default:50). Exported to the engine asGH_AW_MAX_TURNSand additionally passed as--max-turnsto Claude's bare CLI.--retries— from-scratch retries after a "clean exit without verdict" (detector default:0). Timeouts and engine crashes are terminal regardless.Each of these is also readable from an env var:
THREAT_DETECTION_ENGINE_TIMEOUT,THREAT_DETECTION_MAX_TURNS(withGH_AW_MAX_TURNSas a fallback),THREAT_DETECTION_RETRIES.Today the compiled
.lock.ymlinvocation is just:so a workflow author can't override any of these three caps per workflow. The detector's own defaults are already sane, but two use cases need per-workflow tuning:
agent_output.jsonmay need--engine-timeout 10mor--max-turns 100.--engine-timeout 90sto cap credit spend on a burst.Note:
GH_AW_MAX_TURNSis already piped through — the standalone detector reads it as a fallback for--max-turns. So a workflow that setsengine.max-turnsin frontmatter for its main agent already applies the same cap to the detector today, no compiler change needed. This issue is about the two flags that aren't covered (--engine-timeout,--retries) plus a detector-specific override for--max-turnsthat doesn't couple it to the main agent's cap.Proposal
Add a
threat-detection(orsafe-outputs.threat-detection, wherever the existing knobs live) frontmatter section:Compiler behavior:
5m/50/0) then applies. This is the key rule: if the compiler hardcoded the detector's current default onto every emitted line, bumping that default upstream (as we just did going from 20 → 50 on--max-turns) would require every workflow to recompile before picking it up. Emitting nothing on unset keeps the coupling one-way.config_error, but catching it at compile time is a better UX).Resulting invocation for a workflow that sets all three:
Resulting invocation for a workflow that sets none of them (unchanged from today):
Compatibility
gh-aw..lock.ymlmigration needed for workflows that don't opt in.Related