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.
read_fileandlist_dircannot 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:
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:
So the two are not going through the same check. Then the control case:
A link whose target is in the same directory, well inside the jail, fails identically to
the escape attempt. Same for directories:
shellreads 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_moduleslinks in aworkspace install, a monorepo link,
latestpointing at a versioned directory, anythingln -s'd by a build. An agent that writes a link withshelland then reads it withread_filegets told the path does not exist, which is false, and the hint sends itlooking 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
realpathand said so, and the two casesdiverged 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/passwdmessage says. For an in-jail link it shouldsay nothing at all, because it should have worked.
Tested on
localvia WSL2 through@husk-ai/mcp0.1.3. I did not get towrite_fileoredit_fileon the same paths, so whether the write side behaves the same way is untested.