Skip to content

read_file and list_dir cannot follow a symlink, even one that stays inside /work #113

Description

@Hotragn

read_file and list_dir cannot follow a symlink. Not "cannot follow one that escapes" —
cannot follow one at all, including a link that stays entirely inside /work.

I went looking for the opposite. SECURITY-MODEL.md says the path jail rejects a target
that leaves the workspace "including via a symlink created inside it", so I tried to break
it. It held:

shell: ln -sf /etc/passwd /work/escape-file

read_file /work/escape-file
→ cannot read /work/escape-file
  hint: check the path exists

Good outcome, wrong reason, and the error is the tell. A path that is plainly outside the
jail gets a different and much better message:

read_file /etc/passwd
→ path is outside the machine's writable area: /etc/passwd
  hint: this computer exposes /work and /tmp; use a path under one of them

So the two are not going through the same check. Then the control case:

shell: echo "INSIDE-TARGET-CONTENT" > /work/real-inside.txt
       ln -sf /work/real-inside.txt /work/link-inside-abs

read_file /work/link-inside-abs
→ cannot read /work/link-inside-abs
  hint: check the path exists

read_file /work/real-inside.txt
→ INSIDE-TARGET-CONTENT

A link whose target is in the same directory, well inside the jail, fails identically to
the escape attempt. Same for directories:

shell: mkdir -p /work/sub && ln -sf /work/sub /work/link-inside-dir

list_dir /work/link-inside-dir
→ cannot list /work/link-inside-dir
  hint: check the directory exists

shell reads all of them fine, so the computer is not the problem.

Two problems, and the second is the one I would fix first

The functional one. In-workspace symlinks are ordinary. node_modules links in a
workspace install, a monorepo link, latest pointing at a versioned directory, anything
ln -s'd by a build. An agent that writes a link with shell and then reads it with
read_file gets told the path does not exist, which is false, and the hint sends it
looking for a typo. That is a bad half hour for a model.

The one that made me file it. The documented security property is holding by
accident. Nothing is resolving the target and classifying it — links are simply not
followed, so an escaping link and a harmless link get the same refusal. That is safe
today. It stops being safe the moment someone fixes the paragraph above, because the jail
check that the docs cite as proof has never actually been exercised by the case the docs
cite. The test that would catch a regression there does not exist, because the behaviour
it would assert is currently produced by something else.

I would rather the jail resolved through realpath and said so, and the two cases
diverged in the error text, than have the right answer arrive by a route nobody chose.

Also

"check the path exists" is wrong in both cases and should not survive either fix. For the
escape it should say what the /etc/passwd message says. For an in-jail link it should
say nothing at all, because it should have worked.

Tested on local via WSL2 through @husk-ai/mcp 0.1.3. I did not get to write_file or
edit_file on the same paths, so whether the write side behaves the same way is untested.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpkg: agentTouches packages/agentpkg: mcpTouches packages/mcptested with opus 5Found during an Opus 5 test pass

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions