security: sensitive-file checks can be bypassed through symlink aliases

Author: Hao-tian-ZhengCreated Sep 5, 2026Updated Sep 5, 2026

Description

The file-sensitivity policy blocks direct paths such as .env, credentials.json, and server.pem, but it checks the name supplied by the caller without resolving an existing filesystem link first. The Python FileReadTool, FileWriteTool, and ApplyPatchTool therefore follow a harmless-looking symlink to a sensitive file. The Rust file tools use the same basename-only policy.

For example, with notes.txt -> .env:

FileReadTool(path="notes.txt")  -> success, returns the .env contents
FileWriteTool(path="notes.txt") -> success, overwrites .env
ApplyPatchTool(path="notes.txt") -> success, patches .env

This bypasses the policy's promise that sensitive files cannot be accessed through the built-in filesystem tools. allowed_dirs can stop links that leave the allowlisted directory, but the default tools have no directory restriction, so the sensitive-file check still needs to handle aliases itself.

Steps to reproduce

  1. Create a file named .env containing a sentinel value.
  2. Create a symlink named notes.txt pointing to that file.
  3. Call FileReadTool().execute(path=".../notes.txt") (or the write/patch tool).
  4. Observe that the operation succeeds and reaches the .env target.

Expected behavior

Sensitive-file matching should inspect the resolved target for existing paths, so symlink aliases to .env, credentials, private keys, and other protected names are rejected before reading or writing. Nonexistent ordinary paths should keep their current behavior.

Scope

Keep this focused on the shared file-sensitivity boundary and parity between the Python fallback and Rust implementation. Add regression tests for read and write/patch aliases. It does not propose a general directory sandbox or a new dependency. The imported-skill copy boundary is handled separately in #960.

Environment

macOS / Python 3.13.4; reproduced against the current upstream main implementation.