Chio/Docs

Filesystem Guards

Four guards in chio-guards protect the filesystem surface: a forbidden-path denylist, an allowlist counterpart, a secret detector that scans write content, and a patch analyzer that bounds diff size and forbids backdoor patterns. All four operate over a normalized path computed from the request arguments by crate::action::extract_action.

Verdict shape

The kernel verdict is Verdict::Allow / Verdict::Deny / Verdict::PendingApproval. The guards on this page emit only Allow and Deny. None of them carry a payload on the verdict itself; reasons land on the receipt evidence block.

Path Normalization

Every filesystem guard runs the input through three normalizers from crate::path_normalization:

  • normalize_path_for_policy :: lexical normalization (strips . and .., collapses separators, handles Windows backslashes).
  • normalize_path_for_policy_with_fs :: filesystem-resolved normalization (resolves symlinks via the OS).
  • normalize_path_for_policy_lexical_absolute :: absolute lexical normalization without filesystem access.

When the resolved path differs from the lexical path (the symlink-escape case), the guards require the resolved path to match policy. This closes the lexical-bypass hole where a symlink inside an allowed directory points at a denied target.


ForbiddenPathGuard

Blocks file access to sensitive paths via a glob denylist. Source: crates/chio-guards/src/forbidden_path.rs. Guard name: forbidden-path.

Struct

crates/chio-guards/src/forbidden_path.rs
pub struct ForbiddenPathGuard {
    patterns: Vec<glob::Pattern>,
    exceptions: Vec<glob::Pattern>,
}

impl ForbiddenPathGuard {
    pub fn new() -> Self;
    pub fn with_patterns(patterns: Vec<String>, exceptions: Vec<String>) -> Self;
    pub fn is_forbidden(&self, path: &str) -> bool;
}

Default forbidden patterns

Built by default_forbidden_patterns(). These are the actual entries from source, in source order.

crates/chio-guards/src/forbidden_path.rs
// SSH keys
"**/.ssh/**"
"**/id_rsa*"
"**/id_ed25519*"
"**/id_ecdsa*"

// AWS credentials
"**/.aws/**"

// Environment files
"**/.env"
"**/.env.*"

// Git credentials
"**/.git-credentials"
"**/.gitconfig"

// GPG keys
"**/.gnupg/**"

// Kubernetes
"**/.kube/**"

// Docker
"**/.docker/**"

// NPM tokens
"**/.npmrc"

// Password stores
"**/.password-store/**"
"**/pass/**"

// 1Password
"**/.1password/**"

// System paths (Unix)
"/etc/shadow"
"/etc/passwd"
"/etc/sudoers"

// Windows credential stores
"**/AppData/Roaming/Microsoft/Credentials/**"
"**/AppData/Local/Microsoft/Credentials/**"
"**/AppData/Roaming/Microsoft/Vault/**"
"**/NTUSER.DAT"
"**/NTUSER.DAT.*"
"**/Windows/System32/config/SAM"
"**/Windows/System32/config/SECURITY"
"**/Windows/System32/config/SYSTEM"
"**/*.reg"
"**/AppData/Roaming/Microsoft/SystemCertificates/**"
"**/WindowsPowerShell/profile.ps1"
"**/PowerShell/profile.ps1"

Windows globs are kept on Unix; they simply never match. There is no platform-conditional compilation on the pattern set.

Configuration

KnobTypeDefaultPurpose
patternsVec<String>31 globs (above)Deny patterns. Path matches any ⇒ deny.
exceptionsVec<String>emptyGlobs that override a deny. Checked first.

Algorithm

  1. Compute the lexical, resolved (FS-aware), and absolute-lexical paths.
  2. If the resolved path differs from the lexical target (symlink case), exception matching uses only the resolved path. Otherwise either form can match an exception.
  3. For each forbidden pattern, deny if either the resolved or lexical form matches. An exception match short-circuits to allow.

Failure modes & evidence

  • Invalid glob in user input :: silently dropped by filter_map(|p| Pattern::new(p).ok()).
  • Non-filesystem actions (no FileAccess,FileWrite, or Patch in the action enum): guard returns Verdict::Allow.
  • Performance: O(P) glob matches per call, where P is the pattern count. The lexical path computation is allocation-light; the FS-resolved variant performs a single canonicalize syscall on Unix.
chio.yaml
guards:
  forbidden_path:
    patterns:
      - "**/.ssh/**"
      - "**/.env"
      - "/etc/shadow"
    exceptions:
      - "**/fixtures/.env.example"

PathAllowlistGuard

The dual of ForbiddenPathGuard: deny-by-default within an explicit allowlist. Source: crates/chio-guards/src/path_allowlist.rs. Guard name: path-allowlist.

Struct

crates/chio-guards/src/path_allowlist.rs
pub struct PathAllowlistConfig {
    pub enabled: bool,
    pub file_access_allow: Vec<String>,
    pub file_write_allow: Vec<String>,
    pub patch_allow: Vec<String>,
}

pub struct PathAllowlistGuard {
    enabled: bool,
    file_access_allow: Vec<glob::Pattern>,
    file_write_allow: Vec<glob::Pattern>,
    patch_allow: Vec<glob::Pattern>,
}

Configuration

KnobTypeDefaultPurpose
enabledboolfalseDisabled until operator opts in. When off, allows all paths (subject to session-roots check).
file_access_allowVec<String>emptyGlobs that allow read.
file_write_allowVec<String>emptyGlobs that allow write.
patch_allowVec<String>emptyGlobs that allow patch. When empty, falls back to file_write_allow.

Session filesystem roots

When the kernel passes session_filesystem_roots on the GuardContext, the guard also enforces that the path falls within at least one root. This check runs before the enabled flag is consulted, so a session-root violation denies even with the guard disabled. An empty root set means deny (fail-closed).

crates/chio-guards/src/path_allowlist.rs
// Verified from path_allowlist.rs evaluate():
if let Some(session_roots) = ctx.session_filesystem_roots {
    if !self.matches_session_roots(path, session_roots) {
        return Ok(Verdict::Deny);
    }
}

if !self.enabled {
    return Ok(Verdict::Allow);
}

When the FS-resolved path differs from the lexical target (a symlink traversal), the guard requires the resolved path to match the allowlist. This prevents a symlink inside an allowed directory from granting access to a target outside it.


SecretLeakGuard

Scans FileWrite and Patch content for credential leaks. Source: crates/chio-guards/src/secret_leak.rs. Guard name: secret-leak.

Struct

crates/chio-guards/src/secret_leak.rs
pub struct SecretLeakConfig {
    pub enabled: bool,
    pub skip_paths: Vec<String>,
    pub custom_patterns: Vec<CustomSecretPattern>,
}

pub struct CustomSecretPattern {
    pub name: String,
    pub pattern: String,
}

pub struct SecretLeakGuard { /* private */ }

pub struct SecretMatch {
    pub pattern_name: String,
    pub offset: usize,
    pub length: usize,
    pub redacted: String,
}

Default detectors

Names and regex sources verified from default_patterns().

Pattern nameRegex
aws_access_keyAKIA[0-9A-Z]{16}
aws_secret_keycase-insensitive aws_secret_access_key assignment, 40 base64 chars
github_tokengh[ps]_[A-Za-z0-9]{36}
github_patgithub_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}
openai_keysk-[A-Za-z0-9]{48}
openai_project_keysk-proj-[A-Za-z0-9]{48,}
anthropic_keysk-ant-[A-Za-z0-9\\-]{95}
anthropic_api03_keysk-ant-api03-[A-Za-z0-9_\\-]{93}
private_keyPEM -----BEGIN (RSA )?PRIVATE KEY-----
npm_tokennpm_[A-Za-z0-9]{36}
slack_tokenxox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*
stripe_secret_keysk_live_[A-Za-z0-9]{24,}
stripe_restricted_keyrk_live_[A-Za-z0-9]{24,}
gcp_service_accountJSON "type": "service_account"
azure_key_vault_tokencase-insensitive Azure KV assignment, 32+ base64 chars
gitlab_patglpat-[A-Za-z0-9_\\-]{20,}
generic_api_keycase-insensitive api_key= with 32+ alphanum
generic_secretcase-insensitive secret/password/passwd/pwd= with 8+ chars

Default skip paths

From SecretLeakConfig::default():

crates/chio-guards/src/secret_leak.rs
skip_paths: vec![
    "**/test/**",
    "**/tests/**",
    "**/*_test.*",
    "**/*.test.*",
]

Matching paths bypass scanning entirely. The defaults exempt test fixtures so committed example tokens do not block writes.

Configuration

KnobTypeDefaultPurpose
enabledbooltrueMaster switch.
skip_pathsVec<String>4 globs (above)Paths to skip scanning entirely.
custom_patternsVec<CustomSecretPattern>emptyOperator-defined detectors. Compiled at config-load time.

Algorithm

  1. Read action. Only FileWrite(path, content) and Patch(path, diff) proceed. Other actions return Verdict::Allow.
  2. Skip-path check. If any skip glob matches the path, return Allow.
  3. UTF-8 decode the content. Binary content (failed decode) returns empty matches and the call is allowed.
  4. Run every compiled detector regex against the text via find_iter. Any match denies.

Failure modes

  • Invalid custom_patterns regex :: SecretLeakConfigError::InvalidCustomPattern at with_config. The guard will not construct.
  • Built-in regex compile failure :: SecretLeakConfigError::InvalidBuiltInPattern (in practice this should never fire because the patterns are tested).
  • SecretLeakGuard::new() panics if the default config fails to compile. This is treated as a source-level invariant.

Redaction format

Detected secrets are reported via SecretMatch.redacted: the first 4 and last 4 characters of the match are kept, the middle is replaced with *. Matches shorter than 8 chars become all asterisks.


PatchIntegrityGuard

Validates unified-diff payloads before they are applied. Source: crates/chio-guards/src/patch_integrity.rs. Guard name: patch-integrity.

Struct

crates/chio-guards/src/patch_integrity.rs
pub struct PatchIntegrityConfig {
    pub enabled: bool,
    pub max_additions: usize,
    pub max_deletions: usize,
    pub forbidden_patterns: Vec<String>,
    pub require_balance: bool,
    pub max_imbalance_ratio: f64,
}

pub struct PatchAnalysis {
    pub additions: usize,
    pub deletions: usize,
    pub imbalance_ratio: f64,
    pub forbidden_matches: Vec<ForbiddenMatch>,
    pub exceeds_max_additions: bool,
    pub exceeds_max_deletions: bool,
    pub exceeds_imbalance: bool,
}

Defaults

KnobTypeDefaultPurpose
enabledbooltrueMaster switch.
max_additionsusize1000Hard ceiling on added lines per patch.
max_deletionsusize500Hard ceiling on removed lines per patch.
forbidden_patternsVec<String>9 regexes (below)Patterns that, if matched on an added line, deny the patch.
require_balanceboolfalseWhen on, deny if additions/deletions exceed max_imbalance_ratio.
max_imbalance_ratiof6410.0Ratio cutoff (only applied when require_balance).

Default forbidden patterns

crates/chio-guards/src/patch_integrity.rs
// Disable security features
r"(?i)disable[ _\-]?(security|auth|ssl|tls)"
r"(?i)skip[ _\-]?(verify|validation|check)"

// Dangerous operations
r"(?i)rm\s+-rf\s+/"
r"(?i)chmod\s+777"
r"(?i)eval\s*\("
r"(?i)exec\s*\("

// Backdoor indicators
r"(?i)reverse[_\-]?shell"
r"(?i)bind[_\-]?shell"
r"base64[_\-]?decode.*exec"

Algorithm

  1. Iterate diff lines. Lines starting with + (excluding +++ headers) incrementadditions. Lines starting with - (excluding ---) increment deletions.
  2. For each added line, run every forbidden regex. Each hit appends a ForbiddenMatch.
  3. Compute imbalance_ratio = additions / deletions (or f64::INFINITY when deletions are zero and additions are not).
  4. Deny if any forbidden match was found, or any threshold was exceeded.

Failure modes

  • Invalid forbidden_patterns regex :: PatchIntegrityConfigError::InvalidForbiddenPattern at construction. PatchIntegrityGuard::new() panics if the default set fails to compile.
  • Non-Patch actions return Verdict::Allow. The guard does not look at file writes; that is SecretLeakGuard's job.

Composition in the default pipeline

All four guards land in the default pipeline built by GuardPipeline::default_pipeline(). Cheap glob checks run first; the secret scanner and patch analyzer run later because their cost grows with content size.

rust
use chio_guards::{
    ForbiddenPathGuard, PathAllowlistGuard, SecretLeakGuard, PatchIntegrityGuard,
};

let mut pipeline = chio_guards::GuardPipeline::new();
pipeline.add(Box::new(ForbiddenPathGuard::new()));
pipeline.add(Box::new(PathAllowlistGuard::new()));   // disabled by default
pipeline.add(Box::new(SecretLeakGuard::new()));
pipeline.add(Box::new(PatchIntegrityGuard::new()));

PathAllowlistGuard is disabled by default

PathAllowlistGuard::new() constructs with enabled = false and empty allowlists. Until you supply a config via with_config, this guard does not enforce. The session-roots check still runs when the kernel provides session_filesystem_roots.

Next Steps

Filesystem Guards · Chio Docs