Skip to content

Workspace Jailing

Workspace jailing restricts all built-in filesystem tools to a designated directory tree. Agents cannot read, write, or execute outside the workspace boundary — even via path traversal or symlinks.

When a workspace is configured, every filesystem operation goes through resolve_path():

  1. Absolute paths are rejected — agents must use relative paths
  2. Relative paths are joined to the workspace root and normalized
  3. Path traversal (../../etc/passwd) is caught by checking the normalized path stays within the workspace
  4. Symlink escapes are blocked by canonicalizing existing paths and re-checking containment

The workspace root is canonicalized once at startup, so all containment checks use real filesystem paths.

All built-in filesystem tools enforce the jail:

ToolEnforcement
read_filePath must resolve inside workspace
write_filePath must resolve inside workspace
edit_filePath must resolve inside workspace
patchPath must resolve inside workspace
globSearch rooted in workspace
grepSearch rooted in workspace
list_directoryPath must resolve inside workspace
bashWorking directory set to workspace root

Tools without filesystem access (web_fetch, web_search, skill, todo_*, question) are unaffected.

[workspace]
root = "/home/user/project"

If [workspace] is present without a root, it defaults to ~/.heartbit/workspaces.

use heartbit::tool::builtins::{BuiltinToolsConfig, builtin_tools};
use std::path::PathBuf;
let config = BuiltinToolsConfig {
workspace: Some(PathBuf::from("/home/user/project")),
..Default::default()
};
let tools = builtin_tools(config);

Without a config file (env-based), the workspace defaults to the current working directory. The bash tool starts in this directory.

workspace: /home/user/project
/etc/passwd -> ERROR: Absolute paths not allowed
../../etc/passwd -> ERROR: Path escapes workspace root
symlink -> /etc/ -> ERROR: Resolves outside workspace
workspace: /home/user/project
src/main.rs -> /home/user/project/src/main.rs
sub/../file.txt -> /home/user/project/file.txt (internal .. OK)
deeply/nested/file.rs -> /home/user/project/deeply/nested/file.rs

The symlink check uses canonicalize() before the file operation. A symlink could theoretically be swapped between the check and the actual open. This is acceptable for an agent tool jail. Closing this gap fully would require O_NOFOLLOW or OS-level namespaces.

In daemon mode, workspace jailing is critical for multi-tenant isolation. Each task can be assigned a workspace directory, ensuring agents operating on behalf of different tenants cannot access each other’s files.

[workspace]
root = "/var/lib/heartbit/workspaces"

When no workspace is configured:

  • Absolute paths pass through unchanged
  • Relative paths are returned as-is (resolved by the OS relative to CWD)
  • No containment checks are performed

This is the default for CLI standalone mode when no config file is provided.