Defensive guards for desktop apps: path-traversal protection, size limits, and Claude Code headless sandboxing helpers.
See the codeThree 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.
[dependencies]
lloom-guards = "0.1"
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>(())
| Function / constant | What 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_BYTES | 2 MB — text files, config, markdown. |
MAX_IMAGE_BYTES | 10 MB — screenshots, icons. |
DISALLOW_ALL_TOOLS | Slice of every Claude Code tool name. |
disallow_all_tools_arg() | Comma-joined value for --disallowedTools. |
"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.
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_root — creating 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 forWhen 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.
std, no unsafe, no serde / tokio / network crates.tempfile) for the filesystem tests.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.2 commits
Rust
100.0%
Defensive guards for desktop apps: path-traversal protection, size limits, and Claude Code headless sandboxing helpers.
See the codeThree 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.
[dependencies]
lloom-guards = "0.1"
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>(())
| Function / constant | What 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_BYTES | 2 MB — text files, config, markdown. |
MAX_IMAGE_BYTES | 10 MB — screenshots, icons. |
DISALLOW_ALL_TOOLS | Slice of every Claude Code tool name. |
disallow_all_tools_arg() | Comma-joined value for --disallowedTools. |
"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.
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_root — creating 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 forWhen 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.
std, no unsafe, no serde / tokio / network crates.tempfile) for the filesystem tests.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.2 commits
Rust
100.0%