sevenevesai/lloom-guards

Defensive guards for desktop apps: path-traversal protection, size limits, and Claude Code headless sandboxing helpers.

Rust

0

2 commits

updated May 19, 2026

See the code

README

Defensive guards for desktop apps

github crates.io

Three small, reviewable guards extracted from a Tauri desktop app: path-traversal protection, file-size limits, and a disallow-every-tool list for sandboxing headless Claude Code invocations. No network, no subprocess, no unsafe. The only I/O is std::fs::canonicalize inside the path validators.


Install

[dependencies]
lloom-guards = "0.1"

Usage

use std::path::Path;
use lloom_guards::{
    area_path, validate_within_root, validate_new_path_within_root,
    check_size, MAX_FILE_BYTES, disallow_all_tools_arg,
};

// Join a caller-supplied relative path onto a trusted base, rejecting
// `..`, absolute paths, and other non-normal components. No I/O.
let path = area_path(Path::new("/app/scope"), "rules/style.md")?;

// Canonicalize an existing path and ensure it stays under a root.
let resolved = validate_within_root("/app/scope", "/app/scope/rules/style.md")?;

// Same idea for a not-yet-created file (validates the parent).
let new_path = validate_new_path_within_root("/app/scope", "/app/scope/rules/new.md")?;

// Reject oversized payloads with a consistent error message.
check_size(1_500_000, MAX_FILE_BYTES, "File")?;

// Build the `--disallowedTools` argument for a headless `claude -p` call.
let arg = disallow_all_tools_arg();
// → "Bash,Read,Write,Edit,Glob,Grep,NotebookEdit,TodoWrite,WebFetch,WebSearch,Task,Skill,Agent"
# Ok::<(), String>(())

API

Function / constantWhat it does
area_path(base, rel)Join rel onto base, rejecting .., absolute paths, current-dir, and empty input. No filesystem I/O.
validate_within_root(root, child)canonicalize both, return canonical child if it descends from root. Both paths must exist.
validate_new_path_within_root(root, child)Variant for a path that doesn't exist yet — validates the parent, returns <canonical parent>/<file_name>.
same_path(a, b)Case-insensitive, separator-tolerant path equality. Uses canonicalize when both paths exist; falls back to a normalized string compare.
validate_path_in_allowlist(child, &[roots])Canonicalize child and match against any of the canonicalized roots. Missing roots are skipped silently.
check_size(actual, max, kind)Reject if actual > max. Error reports both sizes in MB.
MAX_FILE_BYTES2 MB — text files, config, markdown.
MAX_IMAGE_BYTES10 MB — screenshots, icons.
DISALLOW_ALL_TOOLSSlice of every Claude Code tool name.
disallow_all_tools_arg()Comma-joined value for --disallowedTools.

Trust surface

"If a Tauri command in my app uses these helpers, what could they touch?"

The full surface is the functions above. Path validators call [std::fs::canonicalize] on the inputs you pass them — nothing else. area_path and check_size do no I/O at all. disallow_all_tools_arg returns a string. No network, no subprocess, no writes, no unsafe. Under 300 lines of source.


When to use which path validator

  • area_path — caller-supplied relative path being joined onto a base you control. No filesystem touched, so you don't need either path to exist. Best for IPC commands that take a name or subpath.
  • validate_within_root — existing file or directory being read, renamed, deleted, or otherwise touched in place. Both paths must exist.
  • validate_new_path_within_rootcreating a new file or directory at a caller-specified location. Validates the parent, accepts a non-existent child.
  • validate_path_in_allowlist — multi-root version of validate_within_root. Useful when an app exposes a fixed set of directories (e.g. ~/.claude, <project>/.claude, app data dir).
  • same_path — comparison only, not validation. Handy on Windows where ~/.claude and C:\Users\…\.claude\ should compare equal.

claude_headless — what the disallow list is for

When an app shells out to claude -p for one-shot analysis or content generation (no interactive tool use), every named tool can and should be forbidden via --disallowedTools. The CLI then has no path to read, write, execute, or browse — it just consumes the prompt over stdin and emits text on stdout.

DISALLOW_ALL_TOOLS is a pub const &[&str] of every named tool as of late 2025 / early 2026. Update when Anthropic ships a new tool — the whole point of the disallow-all flag is to keep the headless surface text-in / text-out, so a missing entry would silently widen it.


Scope

  • No GUI coupling, no Tauri dependency, no async runtime.
  • Stable std, no unsafe, no serde / tokio / network crates.
  • One dev-dependency (tempfile) for the filesystem tests.
  • The path validators rely on std::fs::canonicalize, which means TOCTOU is in principle possible if an attacker can race a symlink swap between the check and the subsequent open. For desktop-app scope this is generally acceptable; if it isn't for yours, do the open and metadata check on the same descriptor.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Contributors

sevenevesai

2 commits

sevenevesai/lloom-guards

Defensive guards for desktop apps: path-traversal protection, size limits, and Claude Code headless sandboxing helpers.

Rust

0

2 commits

updated May 19, 2026

See the code

README

Defensive guards for desktop apps

github crates.io

Three small, reviewable guards extracted from a Tauri desktop app: path-traversal protection, file-size limits, and a disallow-every-tool list for sandboxing headless Claude Code invocations. No network, no subprocess, no unsafe. The only I/O is std::fs::canonicalize inside the path validators.


Install

[dependencies]
lloom-guards = "0.1"

Usage

use std::path::Path;
use lloom_guards::{
    area_path, validate_within_root, validate_new_path_within_root,
    check_size, MAX_FILE_BYTES, disallow_all_tools_arg,
};

// Join a caller-supplied relative path onto a trusted base, rejecting
// `..`, absolute paths, and other non-normal components. No I/O.
let path = area_path(Path::new("/app/scope"), "rules/style.md")?;

// Canonicalize an existing path and ensure it stays under a root.
let resolved = validate_within_root("/app/scope", "/app/scope/rules/style.md")?;

// Same idea for a not-yet-created file (validates the parent).
let new_path = validate_new_path_within_root("/app/scope", "/app/scope/rules/new.md")?;

// Reject oversized payloads with a consistent error message.
check_size(1_500_000, MAX_FILE_BYTES, "File")?;

// Build the `--disallowedTools` argument for a headless `claude -p` call.
let arg = disallow_all_tools_arg();
// → "Bash,Read,Write,Edit,Glob,Grep,NotebookEdit,TodoWrite,WebFetch,WebSearch,Task,Skill,Agent"
# Ok::<(), String>(())

API

Function / constantWhat it does
area_path(base, rel)Join rel onto base, rejecting .., absolute paths, current-dir, and empty input. No filesystem I/O.
validate_within_root(root, child)canonicalize both, return canonical child if it descends from root. Both paths must exist.
validate_new_path_within_root(root, child)Variant for a path that doesn't exist yet — validates the parent, returns <canonical parent>/<file_name>.
same_path(a, b)Case-insensitive, separator-tolerant path equality. Uses canonicalize when both paths exist; falls back to a normalized string compare.
validate_path_in_allowlist(child, &[roots])Canonicalize child and match against any of the canonicalized roots. Missing roots are skipped silently.
check_size(actual, max, kind)Reject if actual > max. Error reports both sizes in MB.
MAX_FILE_BYTES2 MB — text files, config, markdown.
MAX_IMAGE_BYTES10 MB — screenshots, icons.
DISALLOW_ALL_TOOLSSlice of every Claude Code tool name.
disallow_all_tools_arg()Comma-joined value for --disallowedTools.

Trust surface

"If a Tauri command in my app uses these helpers, what could they touch?"

The full surface is the functions above. Path validators call [std::fs::canonicalize] on the inputs you pass them — nothing else. area_path and check_size do no I/O at all. disallow_all_tools_arg returns a string. No network, no subprocess, no writes, no unsafe. Under 300 lines of source.


When to use which path validator

  • area_path — caller-supplied relative path being joined onto a base you control. No filesystem touched, so you don't need either path to exist. Best for IPC commands that take a name or subpath.
  • validate_within_root — existing file or directory being read, renamed, deleted, or otherwise touched in place. Both paths must exist.
  • validate_new_path_within_rootcreating a new file or directory at a caller-specified location. Validates the parent, accepts a non-existent child.
  • validate_path_in_allowlist — multi-root version of validate_within_root. Useful when an app exposes a fixed set of directories (e.g. ~/.claude, <project>/.claude, app data dir).
  • same_path — comparison only, not validation. Handy on Windows where ~/.claude and C:\Users\…\.claude\ should compare equal.

claude_headless — what the disallow list is for

When an app shells out to claude -p for one-shot analysis or content generation (no interactive tool use), every named tool can and should be forbidden via --disallowedTools. The CLI then has no path to read, write, execute, or browse — it just consumes the prompt over stdin and emits text on stdout.

DISALLOW_ALL_TOOLS is a pub const &[&str] of every named tool as of late 2025 / early 2026. Update when Anthropic ships a new tool — the whole point of the disallow-all flag is to keep the headless surface text-in / text-out, so a missing entry would silently widen it.


Scope

  • No GUI coupling, no Tauri dependency, no async runtime.
  • Stable std, no unsafe, no serde / tokio / network crates.
  • One dev-dependency (tempfile) for the filesystem tests.
  • The path validators rely on std::fs::canonicalize, which means TOCTOU is in principle possible if an attacker can race a symlink swap between the check and the subsequent open. For desktop-app scope this is generally acceptable; if it isn't for yours, do the open and metadata check on the same descriptor.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Contributors

sevenevesai

2 commits

Languages

Rust

100.0%