const PATH_SUBSTRINGS: &[&str] = &[
".cordance/",
".git/",
".claude/cache/",
".claude/sessions/",
".claude/worktrees/",
".claude/projects/",
".codex/cache/",
".codex/sessions/",
".codex-logs/",
"node_modules/",
"target/",
"dist/",
"build/",
"coverage/",
".pytest_cache/",
"__pycache__/",
".idea/",
".vscode/",
];
const SECRET_FILENAMES: &[&str] = &[
"id_rsa",
"id_ed25519",
"id_ecdsa",
"id_dsa",
"secrets.json",
"secrets.yaml",
"secrets.yml",
"credentials.json",
"credentials.yaml",
".npmrc",
".pypirc",
".netrc",
"gcp-key.json",
];
const OS_JUNK_FILENAMES: &[&str] = &[".ds_store", "thumbs.db"];
const BLOCKED_EXTENSIONS: &[&str] = &[
"log",
"pem",
"key",
"sqlite",
"db",
"profraw",
];
fn has_blocked_extension(final_component: &str, exts: &[&str]) -> bool {
std::path::Path::new(final_component)
.extension()
.and_then(std::ffi::OsStr::to_str)
.is_some_and(|ext| exts.iter().any(|target| ext.eq_ignore_ascii_case(target)))
}
#[must_use]
pub fn is_blocked(rel_path: &str) -> bool {
let p = rel_path.replace('\\', "/");
if PATH_SUBSTRINGS.iter().any(|s| p.contains(s)) {
return true;
}
let final_component = p.rsplit('/').next().unwrap_or(p.as_str());
let final_component_lower = final_component.to_ascii_lowercase();
if final_component_lower == ".git" {
return true;
}
if final_component_lower == ".env" || final_component_lower.starts_with(".env.") {
return true;
}
if SECRET_FILENAMES.contains(&final_component_lower.as_str())
|| OS_JUNK_FILENAMES.contains(&final_component_lower.as_str())
{
return true;
}
if final_component_lower.starts_with("secret") || final_component_lower.ends_with("_secret") {
return true;
}
if final_component_lower.contains(".secret.") || final_component_lower.ends_with(".secret") {
return true;
}
if has_blocked_extension(&final_component_lower, BLOCKED_EXTENSIONS) {
return true;
}
false
}
#[must_use]
pub fn block_reason(rel_path: &str) -> Option<&'static str> {
let p = rel_path.replace('\\', "/");
if p.contains(".cordance/") {
return Some("cordance internal state");
}
if p.contains(".git/") {
return Some("git internal state");
}
if p.contains(".claude/cache/")
|| p.contains(".claude/sessions/")
|| p.contains(".claude/worktrees/")
|| p.contains(".claude/projects/")
{
return Some("claude runtime exhaust");
}
if p.contains(".codex/cache/") || p.contains(".codex/sessions/") || p.contains(".codex-logs/") {
return Some("codex runtime exhaust");
}
if p.contains("node_modules/")
|| p.contains("target/")
|| p.contains("dist/")
|| p.contains("build/")
|| p.contains("coverage/")
|| p.contains(".pytest_cache/")
|| p.contains("__pycache__/")
{
return Some("build / vendor artifact");
}
if p.contains(".idea/") || p.contains(".vscode/") {
return Some("ide state");
}
let final_component = p.rsplit('/').next().unwrap_or(p.as_str());
let final_component_lower = final_component.to_ascii_lowercase();
if final_component_lower == ".git" {
return Some("git internal state");
}
if final_component_lower == ".env" || final_component_lower.starts_with(".env.") {
return Some("environment / secrets file");
}
if SECRET_FILENAMES.contains(&final_component_lower.as_str()) {
return Some("credential material");
}
if OS_JUNK_FILENAMES.contains(&final_component_lower.as_str()) {
return Some("os junk");
}
if final_component_lower.starts_with("secret") || final_component_lower.ends_with("_secret") {
return Some("credential material");
}
if final_component_lower.contains(".secret.") || final_component_lower.ends_with(".secret") {
return Some("credential material");
}
if has_blocked_extension(&final_component_lower, &["pem", "key"]) {
return Some("credential material");
}
if has_blocked_extension(&final_component_lower, &["sqlite", "db"]) {
return Some("binary state");
}
if has_blocked_extension(&final_component_lower, &["log"]) {
return Some("log file");
}
if has_blocked_extension(&final_component_lower, &["profraw"]) {
return Some("coverage exhaust");
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_session_blocked() {
assert!(is_blocked(".claude/sessions/foo.json"));
assert!(is_blocked(".codex-logs/foo.log"));
}
#[test]
fn git_internal_state_blocked() {
assert!(is_blocked(".git/index"));
assert!(is_blocked(".git/config"));
assert!(is_blocked(".git/HEAD"));
assert!(is_blocked(".git/logs/HEAD"));
assert!(is_blocked(".git/packed-refs"));
assert!(is_blocked(".git/objects/pack/pack-abc.pack"));
assert!(is_blocked(".git/gc.log"));
assert_eq!(
block_reason(".git/index"),
Some("git internal state")
);
}
#[test]
fn gitignore_at_root_not_blocked() {
assert!(!is_blocked(".gitignore"));
assert!(!is_blocked(".gitattributes"));
assert!(!is_blocked("subproject/.gitignore"));
}
#[test]
fn bare_git_pointer_file_blocked() {
assert!(is_blocked(".git"));
assert!(is_blocked("subproject/.git"));
assert_eq!(block_reason(".git"), Some("git internal state"));
}
#[test]
fn dot_git_is_exact_not_prefix() {
assert!(!is_blocked(".github"));
assert!(!is_blocked(".github/workflows/ci.yml"));
assert!(!is_blocked("gitlab-ci.yml"));
assert!(!is_blocked("dotgit-helper.sh"));
}
#[test]
fn cordance_internal_state_blocked() {
assert!(is_blocked(".cordance/pack.json"));
assert!(is_blocked(".cordance/sources.lock"));
assert!(is_blocked(".cordance/evidence-map.json"));
assert!(is_blocked(".cordance/cortex-receipt.json"));
assert!(is_blocked(".cordance/llm-candidate.json"));
assert!(is_blocked(".cordance/scan-report.md"));
assert!(is_blocked(".cordance/cache/doctrine/abc/HEAD"));
assert_eq!(
block_reason(".cordance/pack.json"),
Some("cordance internal state")
);
}
#[test]
fn unrelated_cordance_prefix_not_blocked() {
assert!(!is_blocked("docs/cordance-design.md"));
assert!(!is_blocked("src/cordance_helpers.rs"));
}
#[test]
fn repo_tracked_agent_file_not_blocked() {
assert!(!is_blocked(".claude/settings.json"));
assert!(!is_blocked("AGENTS.md"));
assert!(!is_blocked("agents/codex/AGENTS.md"));
}
#[test]
fn dotenv_subdirectory_not_blocked() {
assert!(!is_blocked("crates/.environment/foo.rs"));
assert!(!is_blocked("tests/dotenv-loader.rs"));
assert!(!is_blocked("docs/dotenv-guide.md"));
}
#[test]
fn dotenv_file_blocked() {
assert!(is_blocked(".env"));
assert!(is_blocked("apps/.env"));
assert!(is_blocked(".env.local"));
assert!(is_blocked(".env.production"));
assert!(is_blocked(".env.staging"));
assert!(is_blocked("apps/web/.env.local"));
}
#[test]
fn id_rsa_blocked() {
assert!(is_blocked("id_rsa"));
assert!(is_blocked("~/.ssh/id_rsa"));
assert!(is_blocked(".ssh/id_ed25519"));
assert!(is_blocked("path/to/id_ecdsa"));
assert!(is_blocked("path/to/id_dsa"));
}
#[test]
fn secrets_and_credentials_blocked() {
assert!(is_blocked("secrets.json"));
assert!(is_blocked("config/secrets.yaml"));
assert!(is_blocked("config/secrets.yml"));
assert!(is_blocked("credentials.json"));
assert!(is_blocked("credentials.yaml"));
assert!(is_blocked(".npmrc"));
assert!(is_blocked(".pypirc"));
assert!(is_blocked(".netrc"));
assert!(is_blocked("gcp-key.json"));
}
#[test]
fn secret_prefix_and_suffix_blocked() {
assert!(is_blocked("secret-token.txt"));
assert!(is_blocked("dir/secret-token.txt"));
assert!(is_blocked("path/to/api_secret"));
assert!(is_blocked("api_secret"));
assert!(!is_blocked("secrets/notes.md"));
}
#[test]
fn db_suffix_only_matches_extension() {
assert!(is_blocked("foo.db"));
assert!(!is_blocked("report.dbghelp.txt"));
assert!(!is_blocked("library.dbg.bin"));
}
#[test]
fn log_suffix_blocked() {
assert!(is_blocked("server.log"));
assert!(is_blocked("logs/app.log"));
assert!(!is_blocked("logs.md"));
}
#[test]
fn block_reason_matches_is_blocked() {
let blocked_samples = [
".env",
".env.staging",
"id_rsa",
"secrets.json",
"foo.db",
"server.log",
".claude/sessions/foo.json",
"node_modules/pkg/index.js",
"target/release/cordance",
];
for s in blocked_samples {
assert!(is_blocked(s), "expected blocked: {s}");
assert!(block_reason(s).is_some(), "expected reason for: {s}");
}
}
#[test]
fn block_reason_none_for_clean_paths() {
assert!(block_reason("src/lib.rs").is_none());
assert!(block_reason("docs/adr/0001.md").is_none());
assert!(block_reason("crates/.environment/foo.rs").is_none());
}
#[test]
fn upper_case_secret_prefix_blocked() {
assert!(is_blocked("SECRET-FOO.txt"));
assert!(is_blocked("SECRET-token.txt"));
assert!(is_blocked("Secret-Token.txt"));
assert!(is_blocked("dir/SECRET-FOO.txt"));
assert!(block_reason("SECRET-FOO.txt").is_some());
}
#[test]
fn capitalised_credentials_blocked() {
assert!(is_blocked("Credentials.json"));
assert!(is_blocked("CREDENTIALS.JSON"));
assert!(is_blocked("Secrets.json"));
assert!(is_blocked("SECRETS.YAML"));
assert!(is_blocked("config/Credentials.json"));
assert!(is_blocked("GCP-KEY.JSON"));
assert!(block_reason("Credentials.json") == Some("credential material"));
assert!(block_reason("CREDENTIALS.JSON") == Some("credential material"));
}
#[test]
fn mid_name_caps_secret_blocked() {
assert!(is_blocked("MY.SECRET.txt"));
assert!(is_blocked("my.secret.txt"));
assert!(is_blocked("MY.SECRET")); assert!(is_blocked("foo.SECRET.json"));
assert!(is_blocked("dir/foo.SECRET.json"));
}
#[test]
fn os_junk_case_insensitive() {
assert!(is_blocked(".DS_Store"));
assert!(is_blocked(".ds_store"));
assert!(is_blocked(".DS_STORE"));
assert!(is_blocked("subdir/.DS_Store"));
assert!(is_blocked("Thumbs.db"));
assert!(is_blocked("thumbs.DB"));
assert!(is_blocked("THUMBS.DB"));
assert!(block_reason(".DS_Store") == Some("os junk"));
}
}