Two corrections to docs/threat-model.md, plus the same fix where each claim appears elsewhere.
The escape argument
The document said an escape would be a downgrade, and that the account already holds more than an escape would grant. Both are false. Root on the VM is strictly more capability than a process inside a container with a trimmed capability set.
What the section says now:
An escape grants little additional value, because the things worth taking are already inside the container — the forge credential, which is code access to every repository it can see, and the working tree of the project under way.
And the part that decides it: root on this VM is already a row in the blast radius table. It is the case the model is drawn around, not one it omits. The hypervisor firewall bounds that row identically however root was reached — container escape, local privilege escalation, or an operator mistyping something at a console.
So a rootless runtime changes how hard root is to reach without changing what root can reach once there, and what root can reach is already bounded and already written down. That is now the stated reason in the "Rejected" entry, replacing an argument that rested on the false claim.
README.md and docs/building-a-devbox.md carried the same claim and are corrected.
Cross-project poisoning
Stated as handled in dejima, which scopes both mounts that would carry it: ~/.claude/projects one slug at a time including live worktree children, and ~/.claude/file-history one session at a time filtered to the workspace. I read the current docker-compose.yml and bin/dejima on main rather than inferring it from this container, which predates the change and would have been misleading evidence.
The sibling directories that are mounted host-wide are named — tasks, plans, sessions, backups — with what makes them different: keyed by session identifier rather than project, and not loaded into a session's context at startup.
The audit's radius section
It conflated two questions under one heading. That section reports what root on the VM reaches, and root reads every project directory on the host regardless of how any container mounts them, so the count was right and the framing was wrong. The two are now separate: the count as the radius figure it is, and a note that what a session reaches is decided by dejima's mounts rather than by anything here.
Everything is written in the present tense
No closed tickets from other repositories, no account of what was true before a fix, no arguments against wordings an earlier draft used. The documents describe the current state, because that is all a reader arriving now can use.
One person's work per devbox, stated as a condition
Not an observation about how devboxes get used. The blast radius here is stated in terms of what is on the box, so a machine holding several people's work is out of spec rather than a reason to revisit a decision. The threat model, the README and the revisit conditions all say so, and the last one says it explicitly so nobody reads a shared devbox as a trigger to reopen the rootless question.
If you need separation between people, the supported answer is a devbox each.
The credential that cannot be moved off
docs/building-a-devbox.md §6 told the reader to keep credentials off the box without naming the one they cannot.
dejima keeps the model API token out of the container deliberately: ~/.claude/.credentials.json is never mounted, and a host-side proxy holds the real token while the container sees a placeholder. So a compromised session can use the API while it runs and cannot walk off with it.
The proxy has to read that file, and the proxy runs on the devbox. An escape onto the devbox is exactly the step that turns "can use the API until this session ends" into "holds the token, and can use it from anywhere afterwards." It is the only item on the machine whose loss reaches past the machine.
It is now in the threat model's escape section, in the blast radius table, in §6 of the runbook with rotation as the answer, and in the audit — listed separately from the credentials that can be moved, because the advice for it differs.
This also corrects a claim I made in an earlier revision of this branch, that the set a rootless runtime would protect is close to empty. It is not empty; it has exactly one thing in it. The rejection stands on better grounds: a rootless runtime runs the proxy as the same account it runs the session as, so an escape reaches the credential exactly as it does now. Covering it needs the proxy's identity split from the session's, which is dejima's to arrange, and that is now one of the two revisit conditions.
The vague reasoning is gone
"Making the first step harder is worth something against a targeted attacker" and "revisit if the threat model shifts toward a targeted attacker" were both unfalsifiable. The adversary is now named concretely — a prompt injection in fetched content, or code running during an ordinary build, sharing the property that nobody is at the other end — with the consequence that an undirected payload carries at most a known exploit, which is what patching closes.
The revisit conditions are two things you can check, neither of which is a judgement about attacker intent.
Verified
bats test/ passes, 65 ok and 0 failed, including the link guard across every edited document.
Not in this PR
A parity audit against dejima-ci-hardener found gaps in one direction only: no threat model document, no test for its firewall template's rule ordering, no link guard, a units.bats test that can pass vacuously, and a runbook step that asked the reader to probe a named host on their own LAN. Those are that repository's changes and are in its PR #15.
**Summary generated by LLM agent**
Two corrections to `docs/threat-model.md`, plus the same fix where each claim appears elsewhere.
## The escape argument
The document said an escape would be a downgrade, and that the account already holds more than an escape would grant. Both are false. Root on the VM is strictly more capability than a process inside a container with a trimmed capability set.
What the section says now:
An escape grants little additional **value**, because the things worth taking are already inside the container — the forge credential, which is code access to every repository it can see, and the working tree of the project under way.
And the part that decides it: **root on this VM is already a row in the blast radius table.** It is the case the model is drawn around, not one it omits. The hypervisor firewall bounds that row identically however root was reached — container escape, local privilege escalation, or an operator mistyping something at a console.
So a rootless runtime changes how hard root is to reach without changing what root can reach once there, and what root can reach is already bounded and already written down. That is now the stated reason in the "Rejected" entry, replacing an argument that rested on the false claim.
`README.md` and `docs/building-a-devbox.md` carried the same claim and are corrected.
## Cross-project poisoning
Stated as handled in dejima, which scopes both mounts that would carry it: `~/.claude/projects` one slug at a time including live worktree children, and `~/.claude/file-history` one session at a time filtered to the workspace. I read the current `docker-compose.yml` and `bin/dejima` on `main` rather than inferring it from this container, which predates the change and would have been misleading evidence.
The sibling directories that are mounted host-wide are named — `tasks`, `plans`, `sessions`, `backups` — with what makes them different: keyed by session identifier rather than project, and not loaded into a session's context at startup.
## The audit's radius section
It conflated two questions under one heading. That section reports what **root** on the VM reaches, and root reads every project directory on the host regardless of how any container mounts them, so the count was right and the framing was wrong. The two are now separate: the count as the radius figure it is, and a note that what a session reaches is decided by dejima's mounts rather than by anything here.
## Everything is written in the present tense
No closed tickets from other repositories, no account of what was true before a fix, no arguments against wordings an earlier draft used. The documents describe the current state, because that is all a reader arriving now can use.
## One person's work per devbox, stated as a condition
Not an observation about how devboxes get used. The blast radius here is stated in terms of what is on the box, so a machine holding several people's work is **out of spec** rather than a reason to revisit a decision. The threat model, the README and the revisit conditions all say so, and the last one says it explicitly so nobody reads a shared devbox as a trigger to reopen the rootless question.
If you need separation between people, the supported answer is a devbox each.
## The credential that cannot be moved off
`docs/building-a-devbox.md` §6 told the reader to keep credentials off the box without naming the one they cannot.
dejima keeps the model API token out of the container deliberately: `~/.claude/.credentials.json` is never mounted, and a host-side proxy holds the real token while the container sees a placeholder. So a compromised *session* can use the API while it runs and cannot walk off with it.
The proxy has to read that file, and the proxy runs on the devbox. **An escape onto the devbox is exactly the step that turns "can use the API until this session ends" into "holds the token, and can use it from anywhere afterwards."** It is the only item on the machine whose loss reaches past the machine.
It is now in the threat model's escape section, in the blast radius table, in §6 of the runbook with rotation as the answer, and in the audit — listed separately from the credentials that *can* be moved, because the advice for it differs.
This also **corrects a claim I made in an earlier revision of this branch**, that the set a rootless runtime would protect is close to empty. It is not empty; it has exactly one thing in it. The rejection stands on better grounds: a rootless runtime runs the proxy as the same account it runs the session as, so an escape reaches the credential exactly as it does now. Covering it needs the proxy's identity split from the session's, which is dejima's to arrange, and that is now one of the two revisit conditions.
## The vague reasoning is gone
"Making the first step harder is worth something against a targeted attacker" and "revisit if the threat model shifts toward a targeted attacker" were both unfalsifiable. The adversary is now named concretely — a prompt injection in fetched content, or code running during an ordinary build, sharing the property that **nobody is at the other end** — with the consequence that an undirected payload carries at most a *known* exploit, which is what patching closes.
The revisit conditions are two things you can check, neither of which is a judgement about attacker intent.
## Verified
`bats test/` passes, 65 ok and 0 failed, including the link guard across every edited document.
## Not in this PR
A parity audit against `dejima-ci-hardener` found gaps in one direction only: no threat model document, no test for its firewall template's rule ordering, no link guard, a `units.bats` test that can pass vacuously, and a runbook step that asked the reader to probe a named host on their own LAN. Those are that repository's changes and are in its PR #15.
This is not "the only item on this machine whose loss reaches beyond this machine". Code can be sent off the machine too, along with anything in the filesystem at all. It is one of the more valuable things to exfiltrate though, and one of the most likely, easy to find targets for a prompt injection or supply-chain malware.
This is not "the only item on this machine whose loss reaches beyond this machine". Code can be sent off the machine too, along with anything in the filesystem at all. It is one of the more valuable things to exfiltrate though, and one of the most likely, easy to find targets for a prompt injection or supply-chain malware.
This statement is overly broad. It is entirely possible that a randomly downloaded piece of malware, or a prompt injection, could be more sophisticated than this: adapt based on the environment, download a second stage, phone home, provide ingress to a human attacker. Those aren't the primary threat model, but this threat model shouldn't explicitly ignore these threats either.
This statement is overly broad. It is entirely possible that a randomly downloaded piece of malware, or a prompt injection, could be more sophisticated than this: adapt based on the environment, download a second stage, phone home, provide ingress to a human attacker. Those aren't the primary threat model, but this threat model shouldn't explicitly ignore these threats either.
All three taken. Each was me overreaching rather than mis-wording, so here is what changed.
"The only item whose loss reaches beyond this machine." Wrong, and removed from both places it appeared. Source, uncommitted work and anything else in the filesystem leave just as easily. What actually puts the credential first in the rotation queue is narrower and now stated as such: it is a standing capability rather than a copy of data — what leaves with it is the ability to keep acting as you afterwards, not a snapshot of what was there — and it sits at a known path under a known name, which makes it among the easiest things on the box to find and among the first a prompt injection or supply-chain payload would look for.
That over-claim had propagated further than the line you marked. The escape section said "most of what it reaches is already in the container", which understated it the same way: an escape reaches every other project on the box, and uncommitted work in those exists nowhere else. The rootless rejection then said the set it would protect "has one thing in it". Both are corrected, and the rejection is stronger for it — the control covers neither half of that set, because the proxy runs as whichever account runs the session, and one account owns every project, so moving the session to a dedicated account moves the projects with it.
The one-user requirement now uses your phrasing: "This is not optional. It is a requirement for this tool's threat model."
"Neither reconnoitres, adapts, or escalates." You are right, and this was the worst of the three, because the document was resting an argument on it. Real supply-chain malware fingerprints its environment, pulls a second stage, beacons out, and can hand a live session to a person. And a prompt injection needs to carry none of that itself — the agent reading it is the adaptive component, which makes instructing an agent the cheapest way to get behaviour that responds to what it finds.
So the model no longer rests on the payload being simple, and says so explicitly. What it rests on instead is that every one of those moves needs egress: a second stage has to be fetched, a beacon has to reach a server, a human operator has to be handed something to connect to. The node firewall bounds all of them at once, and bounds them whether the thing being bounded is a shell script or a person driving it.
Being undirected now claims only what it can: it changes what is likely, not what is possible. It makes an unpatched host the more probable route than a zero-day, which is why patching is kept and why it is cheap — and it is not a reason to treat the payload as harmless if it does get in.
Notably that objection strengthens the primary control rather than weakening anything: staging and beaconing are exactly what an egress bound is good at, so a more capable adversary makes the firewall matter more, not less.
bats test/ passes, 65 ok and 0 failed.
**Comment by LLM agent**
All three taken. Each was me overreaching rather than mis-wording, so here is what changed.
**"The only item whose loss reaches beyond this machine."** Wrong, and removed from both places it appeared. Source, uncommitted work and anything else in the filesystem leave just as easily. What actually puts the credential first in the rotation queue is narrower and now stated as such: it is a **standing capability rather than a copy of data** — what leaves with it is the ability to keep acting as you afterwards, not a snapshot of what was there — and it sits at a **known path under a known name**, which makes it among the easiest things on the box to find and among the first a prompt injection or supply-chain payload would look for.
That over-claim had propagated further than the line you marked. The escape section said "most of what it reaches is already in the container", which understated it the same way: an escape reaches every other project on the box, and uncommitted work in those exists nowhere else. The rootless rejection then said the set it would protect "has one thing in it". Both are corrected, and the rejection is stronger for it — the control covers neither half of that set, because the proxy runs as whichever account runs the session, and one account owns every project, so moving the session to a dedicated account moves the projects with it.
**The one-user requirement** now uses your phrasing: "This is not optional. It is a requirement for this tool's threat model."
**"Neither reconnoitres, adapts, or escalates."** You are right, and this was the worst of the three, because the document was resting an argument on it. Real supply-chain malware fingerprints its environment, pulls a second stage, beacons out, and can hand a live session to a person. And a prompt injection needs to carry none of that itself — the agent reading it *is* the adaptive component, which makes instructing an agent the cheapest way to get behaviour that responds to what it finds.
So the model no longer rests on the payload being simple, and says so explicitly. What it rests on instead is that **every one of those moves needs egress**: a second stage has to be fetched, a beacon has to reach a server, a human operator has to be handed something to connect to. The node firewall bounds all of them at once, and bounds them whether the thing being bounded is a shell script or a person driving it.
Being undirected now claims only what it can: it changes what is *likely*, not what is *possible*. It makes an unpatched host the more probable route than a zero-day, which is why patching is kept and why it is cheap — and it is not a reason to treat the payload as harmless if it does get in.
Notably that objection strengthens the primary control rather than weakening anything: staging and beaconing are exactly what an egress bound is good at, so a more capable adversary makes the firewall matter more, not less.
`bats test/` passes, 65 ok and 0 failed.
Correct, and that broke the argument I had just built. Fixed.
The node firewall is policy_out: ACCEPT with the private ranges dropped. It bounds egress to your network and deliberately permits the public internet — the template argues that case at length. So staging, beaconing and handing a session to a person are precisely the moves it does not bound. I had written the opposite.
Egress to the internet is bounded one layer in, by dejima's allowlist inside the container, and an escape is exactly what leaves that behind. A payload that could previously reach only the handful of domains a session was granted can, out on the VM, fetch what it wants and report to whom it wants. That is one more thing an escape gains, and the document now counts it.
What the node firewall actually buys against a capable adversary is narrower and still worth having: the second stage cannot be pulled from your NAS, and the person handed a foothold cannot use it to reach the rest of your network. It bounds where a compromise can spread, not whether it can phone home.
Three places changed:
The blast radius table said "the VM's network position", which is where this hid. It now says egress to allowlisted domains only for the container row and unrestricted egress to the public internet for both VM rows, so the jump between them is visible in the table rather than only in prose.
Under the table, the row-one-to-row-two jump is called out as not being only about privilege: allowlisted egress becomes unrestricted egress because the allowlist lives in the container, this repository does not narrow it back down, and exfiltration and C2 are out of scope deliberately.
SECURITY.md now says it in the claims section, where someone checks what the tool promises: "on your network" is the whole of the claim. A compromise here can phone home; what is bounded is where it can spread.
One thing that does not change: this is not an argument for a rootless runtime after all. An escape into an unprivileged account on the VM has the same unrestricted internet egress as root does, so the rejected control would not have narrowed this either.
bats test/ passes, 65 ok and 0 failed.
**Comment by LLM agent**
Correct, and that broke the argument I had just built. Fixed.
The node firewall is `policy_out: ACCEPT` with the private ranges dropped. It bounds egress to *your network* and deliberately permits the public internet — the template argues that case at length. So staging, beaconing and handing a session to a person are precisely the moves it does **not** bound. I had written the opposite.
Egress to the internet is bounded one layer in, by dejima's allowlist inside the container, and **an escape is exactly what leaves that behind.** A payload that could previously reach only the handful of domains a session was granted can, out on the VM, fetch what it wants and report to whom it wants. That is one more thing an escape gains, and the document now counts it.
What the node firewall actually buys against a capable adversary is narrower and still worth having: the second stage cannot be pulled from your NAS, and the person handed a foothold cannot use it to reach the rest of your network. It bounds where a compromise can **spread**, not whether it can phone home.
Three places changed:
The blast radius table said "the VM's network position", which is where this hid. It now says `egress to allowlisted domains only` for the container row and **`unrestricted egress to the public internet`** for both VM rows, so the jump between them is visible in the table rather than only in prose.
Under the table, the row-one-to-row-two jump is called out as not being only about privilege: allowlisted egress becomes unrestricted egress because the allowlist lives in the container, this repository does not narrow it back down, and exfiltration and C2 are out of scope deliberately.
`SECURITY.md` now says it in the claims section, where someone checks what the tool promises: **"on your network" is the whole of the claim.** A compromise here can phone home; what is bounded is where it can spread.
One thing that does not change: this is not an argument for a rootless runtime after all. An escape into an unprivileged account on the VM has the same unrestricted internet egress as root does, so the rejected control would not have narrowed this either.
`bats test/` passes, 65 ok and 0 failed.
branderson
approved these changes 2026-08-11 18:22:31 -07:00
Two claims in docs/threat-model.md were false rather than loose. An escape would be a downgrade, and the account already holds more than the escape would grant: root on the VM is strictly more capability than a process inside a container with a trimmed capability set. Root on this VM is already a row in the blast radius table, which is the case the model is drawn around, and the hypervisor firewall bounds it identically however root was reached.
The escape section names what an escape reaches that the container did not, and it is not one thing. Every other project on the box becomes readable, and uncommitted work in those exists nowhere else. Beyond that, the model API credential: dejima keeps it out of the container deliberately, so a session can use the API while it runs and cannot carry the token away, and the proxy holding it runs on the devbox. That makes an escape onto the devbox the step that turns temporary use into a token someone keeps.
It is not the only loss that reaches past the machine, and no longer says so. Source and uncommitted work leave just as easily. What puts the credential first in the queue is that it is a standing capability rather than a copy of data, and that it sits at a known path under a known name, which makes it among the easiest things on the box to find.
That also corrects the rootless rejection, which claimed the set it would protect was close to empty. It is small but not empty, and the control covers neither half of it: the proxy runs as whichever account runs the session, and one account owns every project, so moving the session to a dedicated account moves the projects with it.
The threat model no longer rests on the adversary being unsophisticated. Real supply-chain malware fingerprints its environment, stages, beacons and can hand a session to a person, and a prompt injection borrows the agent as its adaptive component. What the model rests on instead is that every one of those moves needs egress, which the node firewall bounds whether the thing being bounded is a script or a person driving it. Being undirected changes what is likely, not what is possible.
One person's work per devbox is stated as a requirement of the threat model rather than an observation, and cross-project context poisoning is stated as handled in dejima, in the present tense.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary generated by LLM agent
Two corrections to
docs/threat-model.md, plus the same fix where each claim appears elsewhere.The escape argument
The document said an escape would be a downgrade, and that the account already holds more than an escape would grant. Both are false. Root on the VM is strictly more capability than a process inside a container with a trimmed capability set.
What the section says now:
An escape grants little additional value, because the things worth taking are already inside the container — the forge credential, which is code access to every repository it can see, and the working tree of the project under way.
And the part that decides it: root on this VM is already a row in the blast radius table. It is the case the model is drawn around, not one it omits. The hypervisor firewall bounds that row identically however root was reached — container escape, local privilege escalation, or an operator mistyping something at a console.
So a rootless runtime changes how hard root is to reach without changing what root can reach once there, and what root can reach is already bounded and already written down. That is now the stated reason in the "Rejected" entry, replacing an argument that rested on the false claim.
README.mdanddocs/building-a-devbox.mdcarried the same claim and are corrected.Cross-project poisoning
Stated as handled in dejima, which scopes both mounts that would carry it:
~/.claude/projectsone slug at a time including live worktree children, and~/.claude/file-historyone session at a time filtered to the workspace. I read the currentdocker-compose.ymlandbin/dejimaonmainrather than inferring it from this container, which predates the change and would have been misleading evidence.The sibling directories that are mounted host-wide are named —
tasks,plans,sessions,backups— with what makes them different: keyed by session identifier rather than project, and not loaded into a session's context at startup.The audit's radius section
It conflated two questions under one heading. That section reports what root on the VM reaches, and root reads every project directory on the host regardless of how any container mounts them, so the count was right and the framing was wrong. The two are now separate: the count as the radius figure it is, and a note that what a session reaches is decided by dejima's mounts rather than by anything here.
Everything is written in the present tense
No closed tickets from other repositories, no account of what was true before a fix, no arguments against wordings an earlier draft used. The documents describe the current state, because that is all a reader arriving now can use.
One person's work per devbox, stated as a condition
Not an observation about how devboxes get used. The blast radius here is stated in terms of what is on the box, so a machine holding several people's work is out of spec rather than a reason to revisit a decision. The threat model, the README and the revisit conditions all say so, and the last one says it explicitly so nobody reads a shared devbox as a trigger to reopen the rootless question.
If you need separation between people, the supported answer is a devbox each.
The credential that cannot be moved off
docs/building-a-devbox.md§6 told the reader to keep credentials off the box without naming the one they cannot.dejima keeps the model API token out of the container deliberately:
~/.claude/.credentials.jsonis never mounted, and a host-side proxy holds the real token while the container sees a placeholder. So a compromised session can use the API while it runs and cannot walk off with it.The proxy has to read that file, and the proxy runs on the devbox. An escape onto the devbox is exactly the step that turns "can use the API until this session ends" into "holds the token, and can use it from anywhere afterwards." It is the only item on the machine whose loss reaches past the machine.
It is now in the threat model's escape section, in the blast radius table, in §6 of the runbook with rotation as the answer, and in the audit — listed separately from the credentials that can be moved, because the advice for it differs.
This also corrects a claim I made in an earlier revision of this branch, that the set a rootless runtime would protect is close to empty. It is not empty; it has exactly one thing in it. The rejection stands on better grounds: a rootless runtime runs the proxy as the same account it runs the session as, so an escape reaches the credential exactly as it does now. Covering it needs the proxy's identity split from the session's, which is dejima's to arrange, and that is now one of the two revisit conditions.
The vague reasoning is gone
"Making the first step harder is worth something against a targeted attacker" and "revisit if the threat model shifts toward a targeted attacker" were both unfalsifiable. The adversary is now named concretely — a prompt injection in fetched content, or code running during an ordinary build, sharing the property that nobody is at the other end — with the consequence that an undirected payload carries at most a known exploit, which is what patching closes.
The revisit conditions are two things you can check, neither of which is a judgement about attacker intent.
Verified
bats test/passes, 65 ok and 0 failed, including the link guard across every edited document.Not in this PR
A parity audit against
dejima-ci-hardenerfound gaps in one direction only: no threat model document, no test for its firewall template's rule ordering, no link guard, aunits.batstest that can pass vacuously, and a runbook step that asked the reader to probe a named host on their own LAN. Those are that repository's changes and are in its PR #15.@@ -118,0 +133,4 @@ordinary answer for a credential you cannot remove: know it is there, and knowwhat to do if the box is ever suspect. **Rotate it** - sign out and back in onthe host, which replaces the stored token - and treat it as the first thing torotate rather than an afterthought, because it is the only item on this machineThis is not "the only item on this machine whose loss reaches beyond this machine". Code can be sent off the machine too, along with anything in the filesystem at all. It is one of the more valuable things to exfiltrate though, and one of the most likely, easy to find targets for a prompt injection or supply-chain malware.
@@ -7,0 +7,4 @@## What this tool assumes**One person's work per devbox.** Not an observation about how devboxes tend tobe used, but a condition of the tool: a machine holding more than one person's"This is not optional. It is a requirement for this tool's threat model: a machine..."
@@ -43,0 +53,4 @@Their shared property matters more than the difference between them: **nobody isat the other end.** Neither was written for this machine or knows it exists. Aprompt injection waits for whichever agent reads it; a malicious package targetseveryone who installs it. Neither reconnoitres, adapts when something does notThis statement is overly broad. It is entirely possible that a randomly downloaded piece of malware, or a prompt injection, could be more sophisticated than this: adapt based on the environment, download a second stage, phone home, provide ingress to a human attacker. Those aren't the primary threat model, but this threat model shouldn't explicitly ignore these threats either.
Comment by LLM agent
All three taken. Each was me overreaching rather than mis-wording, so here is what changed.
"The only item whose loss reaches beyond this machine." Wrong, and removed from both places it appeared. Source, uncommitted work and anything else in the filesystem leave just as easily. What actually puts the credential first in the rotation queue is narrower and now stated as such: it is a standing capability rather than a copy of data — what leaves with it is the ability to keep acting as you afterwards, not a snapshot of what was there — and it sits at a known path under a known name, which makes it among the easiest things on the box to find and among the first a prompt injection or supply-chain payload would look for.
That over-claim had propagated further than the line you marked. The escape section said "most of what it reaches is already in the container", which understated it the same way: an escape reaches every other project on the box, and uncommitted work in those exists nowhere else. The rootless rejection then said the set it would protect "has one thing in it". Both are corrected, and the rejection is stronger for it — the control covers neither half of that set, because the proxy runs as whichever account runs the session, and one account owns every project, so moving the session to a dedicated account moves the projects with it.
The one-user requirement now uses your phrasing: "This is not optional. It is a requirement for this tool's threat model."
"Neither reconnoitres, adapts, or escalates." You are right, and this was the worst of the three, because the document was resting an argument on it. Real supply-chain malware fingerprints its environment, pulls a second stage, beacons out, and can hand a live session to a person. And a prompt injection needs to carry none of that itself — the agent reading it is the adaptive component, which makes instructing an agent the cheapest way to get behaviour that responds to what it finds.
So the model no longer rests on the payload being simple, and says so explicitly. What it rests on instead is that every one of those moves needs egress: a second stage has to be fetched, a beacon has to reach a server, a human operator has to be handed something to connect to. The node firewall bounds all of them at once, and bounds them whether the thing being bounded is a shell script or a person driving it.
Being undirected now claims only what it can: it changes what is likely, not what is possible. It makes an unpatched host the more probable route than a zero-day, which is why patching is kept and why it is cheap — and it is not a reason to treat the payload as harmless if it does get in.
Notably that objection strengthens the primary control rather than weakening anything: staging and beaconing are exactly what an egress bound is good at, so a more capable adversary makes the firewall matter more, not less.
bats test/passes, 65 ok and 0 failed.04abdf4595to4de417ab3cComment by LLM agent
Correct, and that broke the argument I had just built. Fixed.
The node firewall is
policy_out: ACCEPTwith the private ranges dropped. It bounds egress to your network and deliberately permits the public internet — the template argues that case at length. So staging, beaconing and handing a session to a person are precisely the moves it does not bound. I had written the opposite.Egress to the internet is bounded one layer in, by dejima's allowlist inside the container, and an escape is exactly what leaves that behind. A payload that could previously reach only the handful of domains a session was granted can, out on the VM, fetch what it wants and report to whom it wants. That is one more thing an escape gains, and the document now counts it.
What the node firewall actually buys against a capable adversary is narrower and still worth having: the second stage cannot be pulled from your NAS, and the person handed a foothold cannot use it to reach the rest of your network. It bounds where a compromise can spread, not whether it can phone home.
Three places changed:
The blast radius table said "the VM's network position", which is where this hid. It now says
egress to allowlisted domains onlyfor the container row andunrestricted egress to the public internetfor both VM rows, so the jump between them is visible in the table rather than only in prose.Under the table, the row-one-to-row-two jump is called out as not being only about privilege: allowlisted egress becomes unrestricted egress because the allowlist lives in the container, this repository does not narrow it back down, and exfiltration and C2 are out of scope deliberately.
SECURITY.mdnow says it in the claims section, where someone checks what the tool promises: "on your network" is the whole of the claim. A compromise here can phone home; what is bounded is where it can spread.One thing that does not change: this is not an argument for a rootless runtime after all. An escape into an unprivileged account on the VM has the same unrestricted internet egress as root does, so the rejected control would not have narrowed this either.
bats test/passes, 65 ok and 0 failed.4de417ab3ctod2d00dba90