How Harvey‑Labs Prevents Symlink Escape Attacks During Glob and Grep Operations
Harvey‑Labs prevents symlink escape attacks by resolving every candidate file's real path and verifying it stays within the sandbox bind‑mount root before returning results from glob or grep operations.
When AI agents execute filesystem tools inside a containerized sandbox, malicious symlinks pose a serious security risk. The harveyai/harvey-labs repository implements a defense‑in‑depth approach that neutralizes symlink‑escape attacks without requiring agents to understand container boundaries.
The Threat: Symlink Escape in Containerized Tools
A symlink escape attack occurs when an attacker creates a symbolic link inside a restricted directory that points to sensitive files outside that boundary. For example, a malicious agent could create output/leak → /etc/passwd and then use glob or grep to exfiltrate host system data.
Harvey‑Labs addresses this by treating every file discovery as untrusted until proven otherwise. The implementation in harness/tools.py enforces path containment at two critical stages: path translation and result filtering.
Path Translation: Mapping Sandbox to Host
Before any filesystem traversal begins, the Tools class translates sandbox‑relative paths to actual host paths. The method _sandbox_to_host_path performs this mapping in harness/tools.py at line 90.
# From harness/tools.py - path translation before traversal
host_root = self._sandbox_to_host_path(path)
This ensures that glob and grep operations work against the correct bind‑mounted directory, not arbitrary host locations.
The _is_under Guard: Core Protection Mechanism
The critical security primitive is the static method _is_under, implemented at lines 32‑44 of harness/tools.py:
@staticmethod
def _is_under(candidate: Path, root: Path) -> bool:
"""
Return True if candidate is under root (after resolving both).
Does NOT require candidate to exist.
"""
try:
candidate_resolved = candidate.resolve()
root_resolved = root.resolve()
# Attempt to compute relative path - raises ValueError if outside
candidate_resolved.relative_to(root_resolved)
return True
except ValueError:
return False
This method:
- Resolves both paths fully (eliminating symlinks,
., and..components) - Uses
relative_towhich raisesValueErrorwhen the candidate lies outside the root - Requires no file existence checks, making it safe for glob patterns that match non‑existent paths
Glob Implementation with Symlink Protection
The _glob method in harness/tools.py (lines 69‑71) applies this protection to every matched file:
host_root_resolved = host_root.resolve()
result = []
for m in host_root.glob(pattern):
if self._is_under(m, host_root_resolved):
result.append(str(m))
Only files passing the _is_under check are added to results. A symlink pointing outside host_root_resolved fails this test and is silently excluded.
Grep Implementation with Identical Checks
The _grep method follows the same pattern at lines 104‑110:
for f in host_root.glob(glob_pattern):
if not self._is_under(f, host_root_resolved):
continue # Skip escaped symlinks
# Safe to read and search file contents...
This prevents the grep tool from following symlinks that escape the sandbox, even when the pattern otherwise matches them.
Practical Examples
Safe glob execution returning only contained files:
result = tool_executor.execute("glob", '{"pattern": "*.txt"}')
# Returns: ["file1.txt", "file2.txt"]
# Excludes: any .txt file reached via symlink outside /workspace/output
Safe grep that ignores escaped symlinks:
result = tool_executor.execute(
"grep",
'{"pattern": "API_KEY", "path": "/workspace/output"}'
)
# If output/secret_link → /etc/environment, the symlink is ignored
# Only actual files under the bind-mount are searched
Key Source Files
| File | Purpose |
|---|---|
harness/tools.py |
Implements _is_under, _sandbox_to_host_path, _glob, and _grep with symlink protection |
sandbox/sandbox.py |
Defines container mounts at /workspace, /workspace/documents, /workspace/output |
tests/test_sandbox.py |
Validates protection with test_glob_does_not_list_symlink_target_outside_root |
tests/test_pipeline.py |
End‑to‑end verification of glob tool behavior |
Summary
- Path resolution is the defense: Harvey‑Labs prevents symlink escapes by resolving all candidate paths with
Path.resolve()before use - Single verification point: The
_is_understatic method centralizes containment checking for both glob and grep operations - Fail‑secure design: Suspicious files are excluded silently rather than causing errors that might leak information
- No agent cooperation required: Protection happens automatically in the tool implementation, not the agent's prompt or reasoning
Frequently Asked Questions
What happens if an agent creates a symlink to /etc/passwd inside the sandbox?
The symlink is created successfully, but glob and grep operations will not traverse it. When these tools encounter the symlink during directory walking, _is_under resolves it to /etc/passwd, detects the escape outside the bind‑mount root, and excludes it from results.
Does the protection require the symlink target to exist?
No. The _is_under method uses Path.resolve() which works regardless of whether the target exists. This prevents attacks using dangling symlinks or symlinks to future paths.
Are there performance implications for checking every file?
The resolve() operation adds minimal overhead for typical AI agent workloads involving hundreds or thousands of files. The protection runs once per matched file during glob expansion, not per recursive directory entry.
Can agents disable or bypass this protection?
No. The _is_under check is embedded in the Tools class implementation and executes on the host side outside the agent's container. Agents interact only through the tool_executor interface and cannot influence the path validation logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →