use std::path::Path;
use trusty_mpm::core::project_aliases::is_worktree_path;
use trusty_mpm::daemon::managed_routes::inproject::untracked_sync::glob_match;
use super::bash_tokens::RedirectRole;
use super::heredoc::split_heredoc_bodies;
use super::{
PathEnv, redirect_role, resolve_target_path, split_shell_segments, tokenize, unresolved_target,
};
use crate::commands::hook_rewrite::first_command_token;
use crate::commands::pm_guard_secret_handling::body_written_to_a_file;
use crate::commands::pm_guard_secret_read::is_secret_read_target;
pub(crate) const SECRET_FILE_COPY_REASON: &str = "`cp`/`mv` must not copy a secret-shaped file \
(`*.tfvars`, `*.tfvars.json`, `*.tfstate`, `.env*`, `*.pem`, `*.key`, `.netrc`, `*.p12`, \
`*.pfx`, `*.jks`, `*.kdbx`, `token*`, `*.ovpn`, an SSH private key `id_rsa`/`id_dsa`/\
`id_ecdsa`/`id_ed25519` (any suffix), or a name containing `credentials`/`secrets`) into a \
session worktree (issue #7122) — a subsequent directory-wide command (`terraform fmt`, \
`cat`, a formatter) can print its contents, credentials included, into the transcript. \
Reference the file by its absolute path instead (Terraform's `-var-file`/`-state`, a tool's \
`--env-file`), or declare it in the operator's `untracked_sync` allowlist so the daemon \
copies it through the gitignore-verified channel.";
const COPY_VERBS: &[&str] = &["cp", "mv"];
const RENAME_VERBS: &[&str] = &["cp", "mv", "install", "rsync", "ln"];
const SECRET_BEARING_FILE_PATTERNS: &[&str] = &[
"*.tfvars",
"*.tfvars.json",
"*.tfstate",
"*.tfstate.backup",
"*.tfstate.*",
".env",
".env.local",
".env.*",
"*.pem",
"*.key",
"id_rsa*",
"id_dsa*",
"id_ecdsa*",
"id_ed25519*",
"*credentials*",
"*secrets*",
".netrc",
"*.p12",
"*.pfx",
"*.jks",
"*.kdbx",
"token*",
"*.ovpn",
];
const NAME_SUBSTRING_PATTERNS: &[&str] = &["*credentials*", "*secrets*", "token*"];
const PLACEHOLDER_SUFFIXES: &[&str] = &["example", "sample", "template"];
fn is_placeholder_name(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
if !lower.starts_with(".env") {
return false;
}
Path::new(&lower)
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| PLACEHOLDER_SUFFIXES.contains(&e))
}
pub(crate) fn matches_only_name_substring_family(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
let mut matched = false;
for pattern in SECRET_BEARING_FILE_PATTERNS {
if pattern_reaches(pattern, &lower) {
if !NAME_SUBSTRING_PATTERNS.contains(pattern) {
return false;
}
matched = true;
}
}
matched
}
fn pattern_reaches(pattern: &str, lower_candidate: &str) -> bool {
let lower_pattern = pattern.to_ascii_lowercase();
if glob_match(&lower_pattern, lower_candidate) {
return true;
}
if !lower_candidate.bytes().any(|b| b == b'*' || b == b'?') {
return false;
}
let core = pattern_literal_core(&lower_pattern);
if !core.is_empty() && globs_overlap(lower_candidate.as_bytes(), core.as_bytes()) {
return true;
}
lower_candidate.contains('?')
&& globs_overlap(lower_candidate.as_bytes(), lower_pattern.as_bytes())
}
fn is_secret_bearing_name(name: &str) -> bool {
if is_placeholder_name(name) {
return false;
}
let lower = name.to_ascii_lowercase();
SECRET_BEARING_FILE_PATTERNS
.iter()
.any(|pattern| glob_match(&pattern.to_ascii_lowercase(), &lower))
}
fn pattern_literal_core(pattern: &str) -> String {
pattern.replace('*', "")
}
pub(crate) fn secret_pattern_overlaps(candidate: &str) -> bool {
let lower = candidate.to_ascii_lowercase();
if is_placeholder_name(&lower) {
return false;
}
if is_secret_bearing_name(&lower) {
return true;
}
if !lower.bytes().any(|b| b == b'*' || b == b'?') {
return false;
}
SECRET_BEARING_FILE_PATTERNS.iter().any(|pattern| {
let core = pattern_literal_core(&pattern.to_ascii_lowercase());
!core.is_empty() && globs_overlap(lower.as_bytes(), core.as_bytes())
})
}
fn globs_overlap(a: &[u8], b: &[u8]) -> bool {
let (la, lb) = (a.len(), b.len());
let width = lb + 1;
let mut dp = vec![false; (la + 1) * width];
dp[la * width + lb] = true;
for j in (0..lb).rev() {
dp[la * width + j] = b[j] == b'*' && dp[la * width + j + 1];
}
for i in (0..la).rev() {
dp[i * width + lb] = a[i] == b'*' && dp[(i + 1) * width + lb];
for j in (0..lb).rev() {
let hit = if a[i] == b'*' || b[j] == b'*' {
dp[(i + 1) * width + j] || dp[i * width + j + 1]
} else {
(a[i] == b[j] || a[i] == b'?' || b[j] == b'?') && dp[(i + 1) * width + j + 1]
};
dp[i * width + j] = hit;
}
}
dp[0]
}
pub(crate) fn any_pattern_overlaps(candidate: &str) -> bool {
let lower = candidate.to_ascii_lowercase();
if is_placeholder_name(&lower) {
return false;
}
SECRET_BEARING_FILE_PATTERNS
.iter()
.any(|pattern| globs_overlap(lower.as_bytes(), pattern.to_ascii_lowercase().as_bytes()))
}
pub(crate) fn strip_process_substitution(token: &str) -> &str {
let inner = token
.strip_prefix("<(")
.or_else(|| token.strip_prefix(">("))
.or_else(|| token.strip_prefix('('))
.unwrap_or(token);
inner.trim_end_matches(')')
}
pub(crate) fn expand_brace_alternatives(token: &str) -> Option<Vec<String>> {
let Some(start) = token.find('{') else {
return Some(vec![token.to_string()]);
};
let after_open = &token[start + 1..];
let end_rel = after_open.find('}')?;
let alternatives = &after_open[..end_rel];
if !alternatives.contains(',') {
let after_close = start + 1 + end_rel + 1;
let literal_head = &token[..after_close];
let prefix = &token[..start];
let tails = expand_brace_alternatives(&token[after_close..])?;
let elements = expand_sequence(alternatives);
if tails.len().saturating_mul(elements.len() + 1) > BRACE_READING_CAP {
return None;
}
let mut out = Vec::with_capacity(tails.len() * (elements.len() + 1));
for tail in &tails {
out.push(format!("{literal_head}{tail}"));
for element in &elements {
out.push(format!("{prefix}{element}{tail}"));
}
}
return Some(out);
}
if alternatives.contains('{') {
return None;
}
let prefix = &token[..start];
let suffix = &after_open[end_rel + 1..];
let suffix_candidates = expand_brace_alternatives(suffix)?;
let count = alternatives.split(',').count();
if suffix_candidates.len().saturating_mul(count) > BRACE_READING_CAP {
return None;
}
let mut out = Vec::with_capacity(suffix_candidates.len() * count);
for alt in alternatives.split(',') {
for tail in &suffix_candidates {
out.push(format!("{prefix}{alt}{tail}"));
}
}
Some(out)
}
const BRACE_READING_CAP: usize = 4096;
const SEQUENCE_ELEMENT_CAP: usize = 128;
fn expand_sequence(body: &str) -> Vec<String> {
let mut parts = body.split("..");
let (Some(from), Some(to)) = (parts.next(), parts.next()) else {
return Vec::new();
};
if parts.next().is_some() && parts.next().is_some() {
return Vec::new();
}
let one_char = |s: &str| {
let mut chars = s.chars();
match (chars.next(), chars.next()) {
(Some(c), None) if c.is_ascii_alphabetic() => Some(c as u8),
_ => None,
}
};
if let (Some(lo), Some(hi)) = (one_char(from), one_char(to)) {
let (lo, hi) = (lo.min(hi), lo.max(hi));
if usize::from(hi - lo) + 1 > SEQUENCE_ELEMENT_CAP {
return Vec::new();
}
return (lo..=hi).map(|b| (b as char).to_string()).collect();
}
let (Ok(lo), Ok(hi)) = (from.parse::<i64>(), to.parse::<i64>()) else {
return Vec::new();
};
let (lo, hi) = (lo.min(hi), lo.max(hi));
if hi.saturating_sub(lo).saturating_add(1) > SEQUENCE_ELEMENT_CAP as i64 {
return Vec::new();
}
(lo..=hi).map(|n| n.to_string()).collect()
}
pub(crate) fn is_secret_bearing_source(basename: &str) -> bool {
match expand_brace_alternatives(basename) {
Some(candidates) => candidates.iter().any(|c| is_secret_bearing_name(c)),
None => true,
}
}
pub(crate) fn evaluate_secret_file_copy_command(command: &str, cwd: &Path) -> Option<String> {
evaluate_secret_file_copy_command_in(command, cwd, &PathEnv::from_process())
}
fn evaluate_secret_file_copy_command_in(
command: &str,
cwd: &Path,
env: &PathEnv,
) -> Option<String> {
let blanked;
let command = if body_written_to_a_file(command) {
blanked = split_heredoc_bodies(command).0;
blanked.as_str()
} else {
command
};
let mut effective_cwd = cwd.to_path_buf();
for segment in split_shell_segments(command) {
let trimmed = segment.trim();
if trimmed.is_empty() {
continue;
}
if first_command_token(trimmed).as_deref() == Some("cd") {
if let Ok(argv) = tokenize(trimmed)
&& let Some(dest) = argv.get(1)
{
effective_cwd = resolve_target_path(dest, &effective_cwd, env);
}
continue;
}
let Ok(argv) = tokenize(trimmed) else {
continue;
};
if let Some(reason) = laundered_source_rename(&argv) {
return Some(reason);
}
let Some(verb_idx) = argv
.iter()
.position(|tok| COPY_VERBS.contains(&tok.strip_prefix('\\').unwrap_or(tok)))
else {
continue;
};
let tail = &argv[verb_idx + 1..];
let positional = positional_args(tail);
let Some((dest_token, sources)) = positional.split_last() else {
continue;
};
if sources.is_empty() {
continue;
}
let dest_path = resolve_target_path(dest_token, &effective_cwd, env);
let dest_variable = unresolved_target(&dest_path).map(|u| u.token);
if !is_worktree_path(&dest_path) && dest_variable.is_none() {
continue;
}
for source in sources {
let basename = Path::new(source)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or(source.as_str());
if !is_secret_bearing_source(basename) {
continue;
}
return Some(match &dest_variable {
Some(variable) => unresolved_destination_deny_reason(source, dest_token, variable),
None => SECRET_FILE_COPY_REASON.to_string(),
});
}
}
None
}
fn unresolved_destination_deny_reason(source: &str, dest_token: &str, variable: &str) -> String {
format!(
"`cp`/`mv` must not copy the secret-shaped source `{source}` to a destination that still \
carries the unexpanded shell variable `{variable}` (`{dest_token}`) (issue #7122) — the \
guard expands only `$TMPDIR`, `$TMP`, `$HOME` and `$PWD`, so it cannot prove this lands \
outside a session worktree, and it must not allow what it cannot verify. Re-run the \
command with the destination path written out in full, or reference the file by its \
absolute path instead of copying it."
)
}
fn positional_args(tail: &[String]) -> Vec<String> {
let mut out = Vec::new();
let mut positional_only = false;
for tok in tail {
if !positional_only && tok == "--" {
positional_only = true;
continue;
}
if !positional_only && tok.starts_with('-') && tok.len() > 1 {
continue;
}
out.push(tok.clone());
}
out
}
fn token_basename(token: &str) -> &str {
let token = strip_process_substitution(token);
let token = token.strip_prefix('\\').unwrap_or(token);
Path::new(token)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or(token)
}
fn names_a_directory(token: &str) -> bool {
let trimmed = token.trim_end_matches('/');
token.ends_with('/') || trimmed.is_empty() || trimmed == "." || trimmed == ".."
}
fn is_ssh_private_key_name(basename: &str) -> bool {
let lower = basename.to_ascii_lowercase();
["id_rsa", "id_dsa", "id_ecdsa", "id_ed25519"]
.iter()
.any(|family| lower.starts_with(family))
}
fn is_written_as_a_path(token: &str) -> bool {
let stripped = strip_process_substitution(token);
let basename = token_basename(stripped);
stripped.starts_with('/')
|| stripped.starts_with("./")
|| stripped.starts_with("../")
|| stripped.starts_with('~')
|| basename.trim_end_matches('.').contains('.')
|| is_ssh_private_key_name(basename)
}
fn names_an_unreadable_secret(token: &str) -> bool {
is_secret_read_target(token)
}
fn names_an_unreadable_secret_file(token: &str) -> bool {
is_written_as_a_path(token) && names_an_unreadable_secret(token)
}
fn laundered_source_rename(argv: &[String]) -> Option<String> {
renamed_by_a_copy_verb(argv).or_else(|| renamed_by_a_redirection(argv))
}
fn brace_expanded_positionals(positional: &[String]) -> Vec<String> {
positional
.iter()
.flat_map(|token| expand_brace_alternatives(token).unwrap_or_else(|| vec![token.clone()]))
.collect()
}
fn renamed_by_a_copy_verb(argv: &[String]) -> Option<String> {
let verb_idx = argv
.iter()
.position(|tok| RENAME_VERBS.contains(&tok.strip_prefix('\\').unwrap_or(tok)))?;
let verb = argv[verb_idx].strip_prefix('\\').unwrap_or(&argv[verb_idx]);
let positional = brace_expanded_positionals(&positional_args(&argv[verb_idx + 1..]));
let (dest, sources) = positional.split_last()?;
if sources.is_empty() || names_a_directory(dest) {
return None;
}
if expand_brace_alternatives(dest).is_some() && names_an_unreadable_secret(dest) {
return None;
}
let source = sources.iter().find(|source| {
names_an_unreadable_secret_file(source) && source.as_str() != dest.as_str()
})?;
Some(source_rename_deny_reason(
source,
dest,
&format!("`{verb}`"),
))
}
fn renamed_by_a_redirection(argv: &[String]) -> Option<String> {
let dest = redirection_target(argv)?;
if names_a_directory(&dest) || names_an_unreadable_secret(&dest) {
return None;
}
let source = argv
.iter()
.find(|tok| names_an_unreadable_secret_file(tok))?;
Some(source_rename_deny_reason(
source,
&dest,
"a shell redirection",
))
}
fn redirection_target(argv: &[String]) -> Option<String> {
let mut expect_operand = false;
for tok in argv {
if expect_operand {
if tok.starts_with('-') && tok.len() > 1 {
continue;
}
return Some(tok.clone());
}
let bare = tok.strip_prefix('\\').unwrap_or(tok);
if token_basename(bare) == "tee" {
expect_operand = true;
continue;
}
match redirect_role(bare) {
RedirectRole::TargetFollows => expect_operand = true,
RedirectRole::Target(attached) => return Some(attached.to_string()),
RedirectRole::FileDescriptor | RedirectRole::None => {}
}
}
None
}
fn source_rename_deny_reason(source: &str, dest: &str, how: &str) -> String {
format!(
"reproducing the secret-shaped file `{source}` as `{dest}` through {how} is refused \
(issue #7266) — `{dest}` is not itself a name the READ guard refuses, so `cat {dest}`, a \
line range of it and the `Read` tool would all print the credential the guard just \
refused to print from `{source}`. This holds for every destination BASENAME outside that \
file class, whatever its extension and whether or not it lands in a worktree, because \
the read guard cannot tell a laundered name from a real one. A copy that keeps a \
secret-shaped name (`cp .env .env.bak`) and a copy into a directory (`cp .env /tmp/`) \
are untouched. Reference the file by its absolute path instead (`-var-file`, `-state`, \
`--env-file`)."
)
}
#[cfg(test)]
mod tests {
use super::*;
fn env() -> PathEnv {
PathEnv {
tmpdir: None,
tmp: None,
home: Some("/Users/agent".to_string()),
}
}
fn eval(command: &str) -> Option<String> {
evaluate_secret_file_copy_command_in(
command,
Path::new("/repo/.claude/worktrees/agent-x"),
&env(),
)
}
#[test]
fn an_upper_case_secret_file_is_still_a_secret_file_7533() {
for name in [
"server.PEM",
"backup.KEY",
"vault.PFX",
".NETRC",
"TOKEN_STORE",
"AWS_CREDENTIALS",
] {
assert!(
is_secret_bearing_source(name),
"`{name}` is a secret file whatever its casing"
);
}
}
#[test]
fn allows_a_word_family_ending_a_sentence_7533() {
assert_eq!(eval("cat <<'EOF' > note.md\nA bearer token.\nEOF"), None);
assert_eq!(eval("grep -rn credentials... src/ > out.md"), None);
assert!(eval("cp /repo/.env notes.md").is_some());
assert!(eval("cp /repo/credentials.json notes.md").is_some());
assert!(eval("cat /repo/terraform.tfvars > notes.md").is_some());
}
#[test]
fn denies_tfvars_copy_into_a_worktree() {
assert!(
eval("cp ~/live/terraform.tfvars .claude/worktrees/agent-x/terraform.tfvars").is_some()
);
}
#[test]
fn denies_dotenv_copy_into_a_worktree() {
assert!(eval("cp /repo/.env /repo/.claude/worktrees/agent-x/.env").is_some());
}
#[test]
fn denies_pem_copy_into_a_worktree() {
assert!(
eval("cp -p /Users/agent/keys/server.pem /repo/.claude/worktrees/agent-x/").is_some()
);
}
#[test]
fn denies_credentials_named_source() {
assert!(
eval("cp /Users/agent/.aws/credentials /repo/.claude/worktrees/agent-x/credentials")
.is_some()
);
}
#[test]
fn denies_mv_of_a_secret_into_a_worktree() {
assert!(eval("mv /tmp/id_rsa /repo/.claude/worktrees/agent-x/id_rsa").is_some());
}
#[test]
fn denies_case_insensitive_match() {
assert!(
eval("cp /Users/agent/Secrets.JSON /repo/.claude/worktrees/agent-x/Secrets.JSON")
.is_some()
);
}
#[test]
fn allows_secret_copy_to_a_non_worktree_destination() {
assert!(eval("cp /repo/.env /Users/agent/backup/.env").is_none());
}
#[test]
fn allows_ordinary_copy_into_a_worktree() {
assert!(eval("cp README.md /repo/.claude/worktrees/agent-x/README.md").is_none());
}
#[test]
fn allows_secret_copy_hidden_in_an_unparseable_segment() {
assert!(eval("cp 'unterminated .claude/worktrees/agent-x/.env").is_none());
}
#[test]
fn denies_secret_copy_behind_a_tracked_cd() {
assert!(
evaluate_secret_file_copy_command_in(
"cd /repo/.claude/worktrees/agent-x && cp /repo/.env .env",
Path::new("/repo"),
&env(),
)
.is_some()
);
}
#[test]
fn glob_match_is_case_insensitive_via_lowercasing() {
assert!(is_secret_bearing_name("MY.TFVARS.tfvars"));
assert!(is_secret_bearing_name("terraform.TFVARS"));
assert!(!is_secret_bearing_name("README.md"));
}
#[test]
fn denies_brace_expanded_source_copy_into_a_worktree() {
assert!(
eval("cp secret.{tfvars,bak} .claude/worktrees/agent-x/").is_some(),
"one brace alternative (`secret.tfvars`) is secret-shaped, so the whole copy denies"
);
}
#[test]
fn allows_brace_expanded_source_with_no_secret_alternative() {
assert!(
eval("cp notes.{md,txt} .claude/worktrees/agent-x/").is_none(),
"neither brace alternative is secret-shaped"
);
}
#[test]
fn denies_source_with_an_unresolved_brace_group() {
assert!(eval("cp 'notes.{md,txt' .claude/worktrees/agent-x/").is_some());
assert!(eval("cp 'notes.{md,{txt,csv}}' .claude/worktrees/agent-x/").is_some());
}
#[test]
fn denies_a_comma_brace_bomb_past_the_reading_cap() {
assert!(expand_brace_alternatives(&"{a,b}".repeat(13)).is_none());
let started = std::time::Instant::now();
let bomb = format!("notes{}.md", "{a,b}".repeat(30));
assert!(expand_brace_alternatives(&bomb).is_none());
assert!(eval(&format!("cp {bomb} .claude/worktrees/agent-x/")).is_some());
let took = started.elapsed();
assert!(took < std::time::Duration::from_secs(1), "took {took:?}");
}
#[test]
fn denies_secret_copy_to_a_destination_with_unresolved_variable() {
assert!(
evaluate_secret_file_copy_command_in(
"cp secret.tfvars \"$WT/live.tfvars\"",
Path::new("/repo"),
&env(),
)
.is_some()
);
}
#[test]
fn allows_ordinary_copy_to_a_destination_with_unresolved_variable() {
assert!(
evaluate_secret_file_copy_command_in(
"cp README.md \"$WT/README.md\"",
Path::new("/repo"),
&env(),
)
.is_none()
);
}
#[test]
fn denies_every_pattern_in_the_secret_bearing_list() {
let cases: &[(&str, &str)] = &[
("*.tfvars", "prod.tfvars"),
("*.tfvars.json", "prod.tfvars.json"),
("*.tfstate", "terraform.tfstate"),
("*.tfstate.backup", "terraform.tfstate.backup"),
("*.tfstate.*", "terraform.tfstate.20260915-pre.backup"),
(".env", ".env"),
(".env.local", ".env.local"),
(".env.*", ".env.production"),
("*.pem", "server.pem"),
("*.key", "server.key"),
("id_rsa*", "id_rsa"),
("id_dsa*", "id_dsa"),
("id_ecdsa*", "id_ecdsa_sk"),
("id_ed25519*", "id_ed25519.pub"),
("*credentials*", "aws_credentials.json"),
("*secrets*", "app_secrets.yaml"),
(".netrc", ".netrc"),
("*.p12", "client.p12"),
("*.pfx", "client.pfx"),
("*.jks", "keystore.jks"),
("*.kdbx", "vault.kdbx"),
("token*", "token.json"),
("*.ovpn", "client.ovpn"),
];
assert_eq!(
cases.len(),
SECRET_BEARING_FILE_PATTERNS.len(),
"every pattern in the denylist needs exactly one covering case here"
);
for (pattern, sample) in cases {
assert!(
eval(&format!("cp {sample} .claude/worktrees/agent-x/{sample}")).is_some(),
"pattern `{pattern}` sample `{sample}` should have denied"
);
}
}
#[test]
fn allows_non_secret_named_sources() {
for name in ["id_generator.rs", "README.md"] {
assert!(
eval(&format!("cp {name} .claude/worktrees/agent-x/{name}")).is_none(),
"`{name}` shares no shape with `SECRET_BEARING_FILE_PATTERNS` and must not deny"
);
}
}
fn eval_outside_a_worktree(command: &str) -> Option<String> {
evaluate_secret_file_copy_command_in(command, Path::new("/repo"), &env())
}
#[test]
fn denies_a_secret_renamed_to_a_source_extension_outside_a_worktree() {
let reason = eval_outside_a_worktree("cp terraform.tfvars secrets.rs").expect("denies");
assert!(reason.contains("terraform.tfvars"), "{reason}");
assert!(reason.contains("secrets.rs"), "{reason}");
assert!(reason.contains("#7266"), "{reason}");
for cwd in ["/Users/agent", "/tmp"] {
assert!(
evaluate_secret_file_copy_command_in(
"cp /Users/agent/live/terraform.tfvars notes.md",
Path::new(cwd),
&env(),
)
.is_some(),
"cwd `{cwd}` must deny"
);
}
}
#[test]
fn denies_a_secret_renamed_to_a_source_extension_by_any_copy_verb() {
for verb in RENAME_VERBS {
let command = format!("{verb} /Users/agent/.aws/credentials app_config.rs");
assert!(
eval_outside_a_worktree(&command).is_some(),
"`{command}` must deny"
);
}
assert!(eval_outside_a_worktree("install -m 600 server.pem key.rs").is_some());
assert!(eval_outside_a_worktree("ln -s /etc/app/.env config.ts").is_some());
}
#[test]
fn denies_a_secret_redirected_into_a_markup_name() {
for command in [
"cat .env > notes.md",
"cat .env >> notes.md",
"cat .env >notes.md",
"tee notes.md < .env",
"tee -a report.md < /repo/infra/terraform.tfvars",
"base64 server.pem &> dump.txt.md",
] {
assert!(
eval_outside_a_worktree(command).is_some(),
"`{command}` must deny"
);
}
}
#[test]
fn allows_a_secret_copy_that_keeps_its_own_extension() {
assert_eq!(
eval_outside_a_worktree("cp terraform.tfvars backup.tfvars"),
None
);
assert_eq!(eval_outside_a_worktree("cp /repo/.env /tmp/.env.bak"), None);
}
#[test]
fn allows_a_grep_pattern_word_redirected_into_a_markup_name() {
for command in [
"grep -rn credentials src/ > out.md",
"grep -rn secrets crates/ >> audit.md",
"cargo test 2> failures.md",
] {
assert_eq!(eval_outside_a_worktree(command), None, "`{command}`");
}
}
#[test]
fn allows_renaming_an_ordinary_source_file_the_read_guard_already_prints() {
for command in [
"mv crates/trusty-audit/src/grounding/secrets.rs grounding/redacted.rs",
"cp docs/design/UI/design-system/tokens.css tokens.scss",
"mv website/src/lib/theme/tokens.test.ts tokens.spec.ts",
] {
assert_eq!(eval_outside_a_worktree(command), None, "`{command}`");
}
}
#[test]
fn every_pattern_family_is_reachable_by_its_natural_glob() {
let cases: &[(&str, &str)] = &[
("*.tfvars", "*.tfvars"),
("*.tfvars.json", "*.tfvars.json"),
("*.tfstate", "*.tfstate"),
("*.tfstate.backup", "*.tfstate.*"),
("*.tfstate.*", "*.tfstate.*"),
(".env", "*.env"),
(".env.local", "*.local"),
(".env.*", ".env*"),
("*.pem", "*.pem"),
("*.key", "*.key"),
("id_rsa*", "id_*"),
("id_dsa*", "id_d*"),
("id_ecdsa*", "id_ec*"),
("id_ed25519*", "id_ed*"),
("*credentials*", "*credentials*"),
("*secrets*", "*secret*"),
(".netrc", "*.netrc"),
("*.p12", "*.p12"),
("*.pfx", "*.pfx"),
("*.jks", "*.jks"),
("*.kdbx", "*.kdbx"),
("token*", "token*"),
("*.ovpn", "*.ovpn"),
];
assert_eq!(
cases.len(),
SECRET_BEARING_FILE_PATTERNS.len(),
"every pattern in the denylist needs exactly one covering glob here"
);
for (pattern, glob) in cases {
let core = pattern_literal_core(pattern);
assert!(
globs_overlap(glob.as_bytes(), core.as_bytes()),
"glob `{glob}` must target the `{pattern}` family by name (core `{core}`)"
);
assert!(secret_pattern_overlaps(glob), "glob `{glob}` must deny");
}
}
#[test]
fn single_character_wildcards_reach_the_full_entries() {
for candidate in [
"terraform.tfvar?",
"terraform.tfstat?",
"server.pe?",
"vault.kdb?",
] {
assert!(
!secret_pattern_overlaps(candidate),
"`{candidate}` is the gap this exists to close"
);
assert!(
any_pattern_overlaps(candidate),
"`{candidate}` must overlap"
);
}
for candidate in ["file?.txt", "core.?", "notes?.md", "??.rs"] {
assert!(
!any_pattern_overlaps(candidate),
"`{candidate}` must not overlap"
);
}
}
#[test]
fn a_glob_denies_only_when_it_targets_a_family_by_name() {
for glob in [
"*.toml",
"*.json.map",
"*.txt",
"*.log",
"*.csv",
"*.tf",
"*test*",
"*.rs",
"*.md",
"**/*",
"*.yaml",
"*.snap",
"src/**",
"*.html",
] {
assert!(
!secret_pattern_overlaps(glob),
"ordinary glob `{glob}` must not be screened as a secret"
);
}
assert!(secret_pattern_overlaps("*.json"));
}
#[test]
fn name_substring_patterns_are_denylist_entries() {
for pattern in NAME_SUBSTRING_PATTERNS {
assert!(
SECRET_BEARING_FILE_PATTERNS.contains(pattern),
"`{pattern}` must be spelled exactly as its denylist entry"
);
}
}
#[test]
fn matches_only_name_substring_family_separates_word_families_from_file_families() {
for word in [
"credentials",
"secrets",
"token",
"tokens",
"my-credentials",
] {
assert!(
matches_only_name_substring_family(word),
"`{word}` is matched only by a word-shaped family"
);
}
for name in [
"id_rsa",
"id_ed25519",
".netrc",
"live.tfvars",
"server.pem",
] {
assert!(
!matches_only_name_substring_family(name),
"`{name}` names a file family, not a word"
);
}
assert!(!matches_only_name_substring_family("credentials.pem"));
assert!(!matches_only_name_substring_family("README"));
assert!(!matches_only_name_substring_family("Cargo.toml"));
}
#[test]
fn overlap_answers_where_literal_matching_did_not() {
assert!(secret_pattern_overlaps("*.env"));
assert!(secret_pattern_overlaps("*.netrc"));
assert!(
!is_secret_bearing_name("*.env"),
"the literal test misses it"
);
assert!(!is_secret_bearing_name("*.netrc"));
assert!(secret_pattern_overlaps("credentials.rs"));
assert!(secret_pattern_overlaps("terraform.tfvars"));
assert!(!secret_pattern_overlaps("Cargo.toml"));
assert!(!secret_pattern_overlaps("README.md"));
}
#[test]
fn strips_a_process_substitution_wrapper() {
assert_eq!(strip_process_substitution("<(cat"), "cat");
assert_eq!(strip_process_substitution(">(tee"), "tee");
assert_eq!(strip_process_substitution(".env)"), ".env");
assert_eq!(strip_process_substitution("/repo/.env))"), "/repo/.env");
assert_eq!(strip_process_substitution("README.md"), "README.md");
}
#[test]
fn denies_a_secret_renamed_to_an_unsuspicious_name() {
let reason = eval_outside_a_worktree("cp .env ./notes.txt").expect("denies");
assert!(reason.contains(".env"), "{reason}");
assert!(reason.contains("notes.txt"), "{reason}");
for command in [
"cp .env notes.txt",
"mv /repo/infra/terraform.tfvars vars.bin",
"cp ~/.ssh/id_rsa ./deploy_key_backup",
"cat .env > notes.txt",
"tee dump.log < /repo/.env",
] {
assert!(
eval_outside_a_worktree(command).is_some(),
"`{command}` must deny"
);
}
}
#[test]
fn allows_a_secret_copy_into_a_directory() {
for command in [
"cp /repo/.env /Users/agent/backup/",
"cp /repo/infra/terraform.tfvars .",
"mv server.pem ..",
] {
assert_eq!(eval_outside_a_worktree(command), None, "`{command}`");
}
}
#[test]
fn allows_an_npm_install_whose_package_name_is_secret_shaped() {
for command in [
"npm install token-bucket token-bucket",
"npm install token-bucket express",
"npm install @scope/token-bucket express",
"npm install --save-dev secrets-manager lodash",
] {
assert_eq!(eval_outside_a_worktree(command), None, "`{command}`");
}
}
#[test]
fn denies_a_secret_renamed_through_a_brace_group() {
for command in [
"cp {terraform.tfvars,notes.txt}",
"mv {.env,notes.md}",
"cp {/repo/.env,/tmp/dump.bin}",
] {
assert!(
eval_outside_a_worktree(command).is_some(),
"`{command}` must deny"
);
}
for command in [
"cp {a,b}.rs dist/",
"cp Cargo{,.bak}.toml",
"mv {README,NOTES}.md docs/",
] {
assert_eq!(eval_outside_a_worktree(command), None, "`{command}`");
}
}
#[test]
fn denies_a_secret_renamed_to_an_unresolvable_brace_destination() {
let over_cap = format!("git mv terraform.tfvars 'x{}.rs'", "{a,b}".repeat(13));
for command in [over_cap.as_str(), "git mv terraform.tfvars 'x{.rs'"] {
assert!(
eval_outside_a_worktree(command).is_some(),
"`{command}` must deny"
);
}
}
#[test]
fn denies_a_secret_copied_through_process_substitution() {
assert!(eval_outside_a_worktree("cp <(cat .env) notes.txt").is_some());
}
#[test]
fn redirection_target_reads_both_spellings() {
let argv = |s: &str| shlex::split(s).expect("lexes");
assert_eq!(
redirection_target(&argv("cat .env > notes.md")).as_deref(),
Some("notes.md")
);
assert_eq!(
redirection_target(&argv("cat .env >>notes.md")).as_deref(),
Some("notes.md")
);
assert_eq!(
redirection_target(&argv("tee -a notes.md")).as_deref(),
Some("notes.md")
);
assert_eq!(redirection_target(&argv("cat .env")), None);
assert_eq!(redirection_target(&argv("cargo test 2>&1")), None);
assert_eq!(
redirection_target(&argv("cat .env &> notes.md")).as_deref(),
Some("notes.md")
);
}
#[test]
fn redirection_target_reads_a_clobber_destination_8730() {
let argv = |s: &str| shlex::split(s).expect("lexes");
for command in ["cat .env >!notes.md", "cat .env >>! notes.md"] {
assert_eq!(
redirection_target(&argv(command)).as_deref(),
Some("notes.md"),
"{command}"
);
assert!(eval_outside_a_worktree(command).is_some(), "{command}");
}
assert_eq!(eval_outside_a_worktree("cat .env >!.env.bak"), None);
}
}