Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ effort = "" # the Model's variant, passed as #effort; default blank, for OpenC
[security]
fix = false # Security runs may fix reproduced findings; default false
review = false # Runs review their change for Security findings; default false
harness = "" # the Harness a Security run uses unless its command names one; default blank, for harness.default or Claude Code
harness = "" # the Harness a Security run uses unless its command names one; default blank; blank or missing uses harness.default, or Claude Code if harness.default is missing
```

Every key holds its real value, so a Run reading it does exactly what it does with no file. `email.to` has no default, so `setup` suggests one: the public email of your GitHub profile (from `gh api user`), else your global git `user.email`, unless that is a `@users.noreply.github.com` address, which can't receive mail. With neither, `email.to` is the only line written commented out, as above. `setup` never asks `gh` for more scopes, so a private GitHub email is not read, and a Run never looks the suggestion up: `--email` with no address and no `email.to` still stops the Run. From a terminal (stdin and stderr both terminals), `setup` first asks, on stderr:
Expand Down Expand Up @@ -762,7 +762,7 @@ A failed fix keeps its **Claim**, even if it pushed nothing, and stays open for

`email`, optionally followed by an address, or `email.always` in the User config asks for one **Run notification** when the Security run ends, whether the audit succeeded, failed or was interrupted. `no-email` overrides the default. The notification says how the audit ended and lists each finding it recorded or matched to an existing private record: its severity when known, title and private link. It includes no finding write-up, trace or evidence, since it passes through Resend. Detailed failure causes stay in the local logs; the notification gives the audit's status and log paths. A recording failure still lists the records reached before it failed. When a run leaves a reproduced finding unfixed and neither the command nor the User config decided against fixing, its notification and stderr offer both `thirdshift secure security-fix` and `fix = true` under `[security]`. With `no-security-fix`, or the setting turned off, it offers nothing. A skipped Security run sends none. The usual address and Resend API key checks run before the skip checks or any work; a failed send is a warning and never changes the run's outcome.

A Security run chooses its Harness from the command's `harness` word, then `[security] harness` in the User config, then `harness.default`, then Claude Code. A blank or missing Security setting keeps that default. Model and Effort come from the chosen Harness's own `[harness.<name>]` section, with command words taking precedence. For example, `harness claude` overrides `[security] harness = "codex"` and uses `[harness.claude]`.
A Security run chooses its Harness from the command's `harness` word, then `[security] harness` in the User config, then `harness.default`, then Claude Code. When `security.harness` is blank or missing, it uses `harness.default`; when `harness.default` is also missing, it uses Claude Code. Model and Effort come from the chosen Harness's own `[harness.<name>]` section, with command words taking precedence. For example, `harness claude` overrides `[security] harness = "codex"` and uses `[harness.claude]`.

Codex security sessions and their Resumes set `agents.max_concurrent_threads_per_session=8` and ask for fresh sub-agents with `fork_turns: "none"`, so the skill's verifiers stay independent.

Expand Down
2 changes: 1 addition & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ effort = "" # the Model's variant, passed as #effort; default blank, for OpenC
[security]
fix = false # Security runs may fix reproduced findings; default false
review = false # Runs review their change for Security findings; default false
harness = "" # the Harness a Security run uses unless its command names one; default blank, for harness.default or Claude Code
harness = "" # the Harness a Security run uses unless its command names one; default blank; blank or missing uses harness.default, or Claude Code if harness.default is missing
"#;

/// The line `DEFAULTS` holds for `email.to`, which has no default.
Expand Down
15 changes: 8 additions & 7 deletions tests/security_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1897,14 +1897,15 @@ fn a_blank_or_missing_security_harness_preserves_the_default_harness_and_its_set
args.windows(2)
.any(|pair| pair == [json!("-c"), json!("model_reasoning_effort=\"high\"")])
);

let scenario = Scenario::new();
scenario.user_config_is(security);
scenario.agent_does(&audit_script("[]"));
let result = scenario.run(&["secure"]);
assert_eq!(result.code, Some(0), "{security}: {}", result.stderr);
assert_eq!(scenario.claude_calls().len(), 1, "{security}");
assert!(scenario.codex_calls().is_empty(), "{security}");
}
let scenario = Scenario::new();
scenario.user_config_is("[security]\nharness = \"\"\n");
scenario.agent_does(&audit_script("[]"));
let result = scenario.run(&["secure"]);
assert_eq!(result.code, Some(0), "{}", result.stderr);
assert_eq!(scenario.claude_calls().len(), 1);
assert!(scenario.codex_calls().is_empty());
}

#[test]
Expand Down
7 changes: 6 additions & 1 deletion tests/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,12 @@ fn setup_asks_once_about_security_fixing_with_off_as_the_default_and_writes_yes(
let config: toml::Table = text.parse().unwrap();
assert_eq!(config["security"]["fix"].as_bool(), Some(true));
assert_eq!(config["security"]["harness"].as_str(), Some(""));
assert!(text.contains("# the Harness a Security run uses unless its command names one"));
assert!(
text.contains(
"# the Harness a Security run uses unless its command names one; default blank; blank or missing uses harness.default, or Claude Code if harness.default is missing"
),
"{text}"
);
}

#[test]
Expand Down
Loading