You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
claude-code feature mounts ~/.claude read-write, but its README documents read-only mounts for prompt-injection safety #108
The claude-code devcontainer feature's documented security property is not
implemented.
Found incidentally while resolving a networking question on #93's map
(#97) — it is unrelated to that
map's destination, so it is filed here rather than tracked there.
.devcontainer/claude-code/README.md:20-40 documents granular per-file mounts,
with CLAUDE.md, agents/, commands/ and hooks/ mounted read-only, and
gives an explicit prompt-injection argument for why they are read-only.
.devcontainer/claude-code/devcontainer-feature.json instead mounts the whole of ~/.clauderead-write:
So the container can write the host's hooks/, commands/ and CLAUDE.md — the
exact files the README says are read-only because they are executable
instructions that survive into later sessions. The mitigation is documented but
absent.
Worth deciding which of the two is the intention: tighten the mounts to match the
README, or correct the README to stop claiming a protection that is not there.
The first is what the README's own reasoning argues for.
Not verified: whether the granular mount list ever existed and regressed, or was
only ever aspirational. git log on the feature would say.
breadcrumb: direction settled, and it is the first of the two the ticket
offers: tighten the mounts to match the README, not the other way round. The
README's own argument decides it — the files it calls read-only are read-only because they are executable instructions that survive into later sessions, so
deleting the claim would keep the exposure and lose the reason. Decided by the
orchestrator under standing autonomous-work authorisation, recorded here as the
contract this build is held to.
The invariant the build owes: the documented mounts and the shipped mounts
agree, and every executable-instruction path is read-only. If a path turns out
to need writing at runtime, the faithful variant is a narrower writable set with
the instruction files still read-only, and the README restated to say exactly
what ships — never a README that promises a protection the manifest does not
carry.
One thing the ticket left unverified is now verified: the granular mount list
never existed. The feature manifest has had exactly one revision since the
initial commit, and it mounts the whole config directory read-write. The
protection was aspirational from the first day rather than a regression.
Seams this will be tested at, in order of how much they can be trusted:
The feature manifest's mount list, read as data. Every path the README
calls protected is mounted read-only; the whole-directory read-write mount is
gone; the writable set is exactly the two state files the README names as
necessary. Stated as rules over the list, so a sixth mount cannot arrive
writable without failing.
The README and the manifest, parsed from both sides. The defect this
ticket reports is precisely that those two drifted, so the test that matters
is the one that makes them move together — a documented path with no mount,
or a mount with no documentation, is a failure.
The host-side pre-create hook. A bind mount whose source is missing is
not a degraded container, it is no container; the same hook that already
creates the ssh file this repo mounts is where every newly mounted config
path gets created too, idempotently, since it has to survive running inside
the container it built.
A real container. The first three are all statements about text. Whether
a devcontainer feature's mount entries reach docker as read-only binds is a
question only a running container answers, so one gets built from this
feature and the protection is checked by trying to write through it.
breadcrumb: the mechanism is measured rather than inferred, and one of the
measurements changed what shipped.
A Feature's readonly really does reach Docker. This was worth checking:
the published Feature schema has no readonly at all — a mount there is an
object of source, target and type, additionalProperties: false, and the string
form this feature has always used is not in the schema either — while every
implementation accepts it. Built against a throwaway host directory, the
container's own /proc/mounts reports ro for all five protected paths and rw
for the two state files, and a write to each protected path is refused with Read-only file system — as root. The host's files are unchanged afterwards, so
the refusal is a refusal and not a write into a copy.
Nothing Claude needs to write is read-only. The question the ticket flagged —
sessions, todos, history, projects all live under the same directory — resolves
the easy way: those paths are simply not mounted, so they are the container's own
and writable. The mount list is an allow-list rather than a directory with holes
in it, which is also why a directory Claude starts writing to next month cannot
quietly become another way onto the host.
The cost is real and is now written down. A container no longer resumes a
session started on the host, and the host's skills and plugins are not visible in
there. Mounting those read-only is the same argument and would be a reasonable
follow-up; it is deliberately not in this change.
The missing-path worry turned out not to be a new one. A bind whose source is
missing is refused before any container exists — nothing is created on the host,
so the feared root-owned directory named CLAUDE.md does not happen. And the
whole-directory mount that preceded this failed in exactly the same way on a host
with no configuration directory, measured both ways. The list the pre-create hook
has to create is longer; the failure mode is not new.
One thing the mounts made newly load-bearing. The pre-create hook runs inside
containers built this way, where the paths it would create are the read-only
mounts — so every line of it is guarded on absence, and that is now checked in a
real container instead of by proxy.
breadcrumb: built and open as PR #219 — the feature now mounts the paths its
README documents, with the executable-instruction files read-only, and everything
else under the configuration directory is the container's own.
Held by seven unit tests that require the README and the manifest to agree in
both directions rather than keeping a third copy of the list, and one e2e test
that proves the protection in a real container in about twenty seconds — built
against a scratch host directory, so it can attempt the writes for real without
any run of it, passing or failing, being able to touch a developer's
configuration. Both suites were mutation-checked (thirteen mutations: flag
dropped, mount removed, target skewed, whole-directory mount restored, a README
bullet deleted, the hook writing unconditionally, and more) and every one is
caught by the test that should catch it.
Every check on the PR is green, including the e2e job — so the protection is
confirmed on a second machine that is not the one it was developed on.
wf-review verdict on #219: Approve — both axes clean; the documented read-only protection now genuinely ships (manifest↔README cross-checked line by line, mutations reproduced, e2e verified in a real container by builder + CI + reviewer independently). Doc residue both axes flagged (install.sh's stale banner, incomplete publishing recipe) closed during review as 45cecab. Merging once checks re-green.
Activity
blooop commentedon Aug 15, 2026
breadcrumb: direction settled, and it is the first of the two the ticket
offers: tighten the mounts to match the README, not the other way round. The
README's own argument decides it — the files it calls read-only are read-only
because they are executable instructions that survive into later sessions, so
deleting the claim would keep the exposure and lose the reason. Decided by the
orchestrator under standing autonomous-work authorisation, recorded here as the
contract this build is held to.
The invariant the build owes: the documented mounts and the shipped mounts
agree, and every executable-instruction path is read-only. If a path turns out
to need writing at runtime, the faithful variant is a narrower writable set with
the instruction files still read-only, and the README restated to say exactly
what ships — never a README that promises a protection the manifest does not
carry.
One thing the ticket left unverified is now verified: the granular mount list
never existed. The feature manifest has had exactly one revision since the
initial commit, and it mounts the whole config directory read-write. The
protection was aspirational from the first day rather than a regression.
Seams this will be tested at, in order of how much they can be trusted:
calls protected is mounted read-only; the whole-directory read-write mount is
gone; the writable set is exactly the two state files the README names as
necessary. Stated as rules over the list, so a sixth mount cannot arrive
writable without failing.
ticket reports is precisely that those two drifted, so the test that matters
is the one that makes them move together — a documented path with no mount,
or a mount with no documentation, is a failure.
not a degraded container, it is no container; the same hook that already
creates the ssh file this repo mounts is where every newly mounted config
path gets created too, idempotently, since it has to survive running inside
the container it built.
a devcontainer feature's mount entries reach docker as read-only binds is a
question only a running container answers, so one gets built from this
feature and the protection is checked by trying to write through it.
blooop commentedon Aug 15, 2026
breadcrumb: the mechanism is measured rather than inferred, and one of the
measurements changed what shipped.
A Feature's
readonlyreally does reach Docker. This was worth checking:the published Feature schema has no
readonlyat all — a mount there is anobject of source, target and type,
additionalProperties: false, and the stringform this feature has always used is not in the schema either — while every
implementation accepts it. Built against a throwaway host directory, the
container's own
/proc/mountsreportsrofor all five protected paths andrwfor the two state files, and a write to each protected path is refused with
Read-only file system— as root. The host's files are unchanged afterwards, sothe refusal is a refusal and not a write into a copy.
Nothing Claude needs to write is read-only. The question the ticket flagged —
sessions, todos, history, projects all live under the same directory — resolves
the easy way: those paths are simply not mounted, so they are the container's own
and writable. The mount list is an allow-list rather than a directory with holes
in it, which is also why a directory Claude starts writing to next month cannot
quietly become another way onto the host.
The cost is real and is now written down. A container no longer resumes a
session started on the host, and the host's skills and plugins are not visible in
there. Mounting those read-only is the same argument and would be a reasonable
follow-up; it is deliberately not in this change.
The missing-path worry turned out not to be a new one. A bind whose source is
missing is refused before any container exists — nothing is created on the host,
so the feared root-owned directory named
CLAUDE.mddoes not happen. And thewhole-directory mount that preceded this failed in exactly the same way on a host
with no configuration directory, measured both ways. The list the pre-create hook
has to create is longer; the failure mode is not new.
One thing the mounts made newly load-bearing. The pre-create hook runs inside
containers built this way, where the paths it would create are the read-only
mounts — so every line of it is guarded on absence, and that is now checked in a
real container instead of by proxy.
blooop commentedon Aug 15, 2026
breadcrumb: built and open as PR #219 — the feature now mounts the paths its
README documents, with the executable-instruction files read-only, and everything
else under the configuration directory is the container's own.
Held by seven unit tests that require the README and the manifest to agree in
both directions rather than keeping a third copy of the list, and one e2e test
that proves the protection in a real container in about twenty seconds — built
against a scratch host directory, so it can attempt the writes for real without
any run of it, passing or failing, being able to touch a developer's
configuration. Both suites were mutation-checked (thirteen mutations: flag
dropped, mount removed, target skewed, whole-directory mount restored, a README
bullet deleted, the hook writing unconditionally, and more) and every one is
caught by the test that should catch it.
Every check on the PR is green, including the e2e job — so the protection is
confirmed on a second machine that is not the one it was developed on.
blooop commentedon Aug 15, 2026
wf-review verdict on #219: Approve — both axes clean; the documented read-only protection now genuinely ships (manifest↔README cross-checked line by line, mutations reproduced, e2e verified in a real container by builder + CI + reviewer independently). Doc residue both axes flagged (install.sh's stale banner, incomplete publishing recipe) closed during review as 45cecab. Merging once checks re-green.