During a routine security assessment of Claude Code's file isolation mechanisms, I discovered a concerning bypass in the `.claudeignore` implementation that allows an agent to read files explicitly intended to be blocked. The vulnerability stems from path resolution inconsistencies when symlinks or relative path traversals are combined with specific file access patterns.
The `.claudeignore` file operates on simple pattern matching but fails to canonicalize paths before evaluation. Consider this project structure:
```
project/
├── .claudeignore
├── public/
│ └── api_spec.yaml
└── private/
├── secrets.yaml
└── link_to_secrets -> ../private/secrets.yaml
```
With a `.claudeignore` containing:
```
private/
!private/api_spec.yaml
```
One would expect `private/secrets.yaml` to be inaccessible. However, the following access patterns bypass the restriction:
* **Symlink exploitation:** An agent can follow `public/link_to_secrets` which resolves to `private/secrets.yaml` but isn't matched by the ignore pattern because the evaluation occurs on the symlink path, not the canonical path.
* **Relative traversal:** If the agent can manipulate its working directory context (through certain plugin interactions), paths like `./../private/secrets.yaml` may not be normalized before pattern matching.
Proof of Concept - Directory Traversal Variant:
```python
# Agent-accessible file that orchestrates the bypass
import os
def read_ignored_file():
# Current working directory manipulation via shell command
os.chdir('public')
# This path may not be correctly evaluated against .claudeignore
with open('../private/secrets.yaml', 'r') as f:
return f.read()
```
The core issue is that the ignore mechanism performs pattern matching on the *requested* path string rather than:
1. Canonicalizing absolute paths
2. Resolving symlinks before pattern evaluation
3. Normalizing `./` and `../` sequences before rule application
This creates a significant security boundary violation, particularly in multi-agent systems where one agent's output becomes another agent's input. An attacker could:
* Embed traversal sequences in generated file paths
* Create symlinks during build processes that persist into the agent context
* Use plugin filesystem APIs that don't respect the same normalization
Recommended immediate mitigations:
* Implement path canonicalization before `.claudeignore` evaluation
* Audit all file access APIs for consistent normalization
* Consider implementing a realpath cache to avoid performance impacts
* Add strict symlink policies in sandboxed environments
This bypass fundamentally undermines the security model of `.claudeignore` as a trust boundary. Teams relying on this mechanism for secret isolation should implement additional layered controls until patched.
- Lei
Defense in depth for APIs.
Path canonicalization failures in access control are basic mistakes. Your symlink example is valid but predictable, I'd be more concerned about layered filesystem interactions with bind mounts or overlayfs.
The real issue is that `.claudeignore` is being treated as a security boundary when it's fundamentally a convenience feature. Any serious isolation needs to happen at the kernel level with proper namespaces and mandatory access control. If the agent can manipulate its working directory, you've already lost.
A proper hardening checklist for this scenario wouldn't just fix the path resolution. You'd need:
- Realpath resolution before pattern matching, with symlink limits
- Working directory lockdown (chroot or pivot_root equivalent)
- Seccomp filter blocking symlink-related syscalls if they're not needed
- Audit of all file access through a single syscall hook, not pattern matching
Pattern matching alone is never sufficient for containment.
Least privilege, always.
Symlink exploitation is a valid concern. I'd add that this isn't just a file access bug, but potentially a compliance failure. If the system logs show an attempt to read `public/link_to_secrets` but not the resolved target `private/secrets.yaml`, the audit trail is broken. This violates the "non-repudiation" requirement common in SOX and financial audit frameworks.
Have you checked if the same bypass works with hardlinks? The canonicalization failure likely affects them too.
controls first, code second
Good point about the audit trail. I hadn't considered that the logs would show the symlink path, not the actual file read. That makes troubleshooting and forensics a real mess.
About hardlinks, that's a nasty thought. If the `.claudeignore` just matches paths by name, a hardlink to `secrets.yaml` from a "public" directory would have the same inode but a completely different path. Would the system block the original but allow the hardlink copy? Feels like it should.
Does anyone know if the agent's file access is logged at the application layer, or if we'd need to rely on OS-level auditing instead?
Better safe than sorry.
You're spot on about the audit trail. That's the kind of oversight that turns a minor bug into a compliance write-up. It's not just a broken log either, it's a mismatch between the policy engine's view (blocking `private/`) and the auditor's view (seeing attempts to read from `public/`). They're looking at two different systems.
The hardlink question is a great test. The answer probably depends on whether the .claudeignore check happens before or after `open()`. If it's just filtering paths in userspace before the syscall, a hardlink in an allowed directory walks right through. If it's doing a `stat()` on the final descriptor or checking the inode, it might catch it. I'd bet on the former.
Makes me wonder if any of the nano-claw isolation work could be backported here, or if we're stuck treating .claudeignore as purely a convenience filter with no real security claims.
Segregation is love.