{"slug": "your-sandbox-wraps-a-process-but-your-agent-now-lives-behind-a-service", "title": "Your Sandbox Wraps a Process, but Your Agent Now Lives Behind a Service", "summary": "OpenCode 2.0, released 12 October 2026, now runs its server automatically as a single background process that all TUIs, web interfaces and desktop clients connect to, meaning a sandbox wrapped around the launched opencode client no longer contains the process that reads files and runs commands. Najam us Saqib's writeup, cited by the author, demonstrates a client inside a read-only bubblewrap sandbox reaching the service over its local API and creating a file on the host outside the sandbox, and points to fixed service port 49374 and TNG issue #328 as factors easing autodiscovery from a confined client; the recommended mitigations are running the agent with --standalone or giving each sandbox a private service instance. The author states the accompanying .NET plus Docker test bench covering five escapes was not executed end to end and its results table is labelled expected behavior, not measured runs.", "body_md": "I have been building a sandbox around my own coding agent for a few months, and I had quietly assumed the hard part was the kernel stuff: seccomp, read-only mounts, dropped capabilities, egress rules. Then I read Najam us Saqib’s writeup of OpenCode 2.0 and realized I had been guarding the wrong object. My sandbox wrapped a process. The thing I actually needed to contain had moved out of that process and into a shared service on the same machine.\n\nThis is the writeup of that correction: where the boundary really is for a coding agent in late 2026, two concrete ways it has already broken this year, and a .NET plus Docker test bench I built to check my own setup against five specific escapes. I will be honest about one thing up front, because the project rule I write under demands it and because the topic demands it more: I did not execute the bench end to end for this article. The code is complete and copy-paste ready, and the results table is labelled as expected behavior derived from each configuration, not as measured runs. I will mark that line clearly every time it matters, and the whole point of shipping the bench is that you run it yourself and tell me where my reasoning was wrong.\n\nNajam’s piece is titled, bluntly, “OpenCode 2.0 Turned Your Coding Agent Into a Daemon.” The architecture claim in it is confirmed by OpenCode’s own 2.0 release note, dated 12 October 2026, so I am treating the shape of the change as fact and the exploit specifics as his demonstration.\n\nHere is the shape, in OpenCode’s own words from that release note. OpenCode always had a client/server split, but it used not to lean on it: “From the beginning OpenCode adopted a client/server model. However, this was not something it took advantage of by default.” In 2.0 that changed. “In 2.0 this server now automatically runs as a single background process on your machine.” Every interface you open is a client of that one process: “When you open up a TUI, the web interface or the desktop app they’re all talking to the same central process.” And clients find it on their own: scripts “will autodiscover the local service and just work.” You can even “expose it to other hosts and run /pair or opencode pair to connect it to other applications.\"\n\nRead that again with a sandbox in mind. In 1.x, if you wrapped the opencode process in a sandbox, you wrapped the thing that read your files and ran your commands. In 2.0, the thing that reads your files and runs your commands is the background service, and the opencode you launched inside your sandbox is just a client that talks to it. Sandbox the client all you like. The privileged work happens in a process your sandbox never touched.\n\nNajam’s demonstration, as he describes it, is exactly this: a client running inside a read-only bubblewrap sandbox reaches the service over its local API and creates a file on the host, outside the sandbox, because the service is the one doing the writing and the service was never confined. The read-only bubble is real and it is doing nothing, because the privileged actor is on the other end of a socket.\n\nTwo more specifics from his piece, which I am labelling as his findings because I could not pull them from the release note and did not independently reproduce them. He points at a fixed service port, 49374, and references TNG issue #328 discussing that port under a Landlock-based sandbox, where a fixed, predictable port is what makes autodiscovery from inside a confined client so easy. And his recommended fixes are the two that follow directly from the architecture: run the agent with --standalone so it does not detach into a shared background service, or give each sandbox its own private service instance instead of letting every client share one host-wide daemon.\n\nI want to be careful here. I am not reporting a CVE. OpenCode shipped a feature, a single shared service with autodiscovery, that is genuinely convenient and that quietly invalidates a class of sandbox people had already built around 1.x. That is the interesting part. The boundary moved and the old wrapper stayed where it was.\n\nWhile the OpenCode story is about an architectural boundary moving, the Cursor story is about the sandbox’s own allowlist being talked into widening itself. Cato AI Labs named it DuneSlide, two flaws in Cursor’s agent sandbox, and I went to the primary coverage before writing anything down.\n\nThe facts I am confident in: both are tracked as CVE-2026–50548 and CVE-2026–50549, both rated 9.8 on CVSS 3.1 (9.3 on 4.0), both fixed in Cursor 3.0 and present in every version before it. Cato reported them on 19 February 2026; the CVE IDs were assigned on 5 June; the public writeup I read is dated 1 July. No known exploitation in the wild was reported. Those severity and version numbers are from the vendor and NVD, and I am labelling them as such.\n\nThe mechanisms are the part worth internalizing, because they are both the same move in different clothes: untrusted input reaches into the sandbox’s own configuration and edits the fence from the inside.\n\nCVE-2026–50548 is the settings one. Cursor’s run_terminal_cmd tool takes an optional working_directory parameter, and the sandbox adds that directory to its allowed-write list. The tool argument is agent-controlled, and the agent can be steered by injected text in anything it reads. So a prompt injection sets working_directory to a system path, the sandbox dutifully adds that path to the write allowlist, and now the attacker can overwrite the sandbox helper itself or a shell startup file like ~/.zshrc, after which later commands run unsandboxed. The allowlist did its job perfectly. It allowed exactly what it was told to allow. The bug is that what it was told came from the thing it was supposed to be containing.\n\nCVE-2026–50549 is the symlink one. Cursor resolves symlinks to check that a write target really lands inside the project. The flaw is the failure path: if the canonicalization check fails, Cursor falls back to trusting the literal in-project path. So you plant a symlink that points outside the project, you make the resolution fail, and the fallback writes through your symlink to the sandbox helper. A security check that fails open is not a check. It is a delay.\n\nBoth of these are what people are now calling the lethal trifecta: the agent has access to private data (your filesystem), it is exposed to untrusted content (web results, MCP tool output), and it has a way to exfiltrate or act (write files, run commands). When all three overlap in one agent, a single injected string is a remote code execution primitive, and no click or approval is needed. DuneSlide is that trifecta landing on the sandbox’s config surface specifically.\n\nThe through-line from OpenCode to Cursor is this: in both cases the sandbox was present and technically working, and in both cases the real boundary was somewhere the sandbox did not cover. One moved the privileged work to a service. The other let the fence be redrawn by its own prisoner.\n\nOne more, because it reframes what “blast radius” means. In mid-October 2026, OpenCode disclosed a breach of its OpenCode Console, the hosted side of the product, that exposed third-party AI provider API keys users had linked to it. Everything in this section is from newsletter and press coverage plus OpenCode’s own posts, so I am labelling the numbers as reported, not verified.\n\nReported facts: an intruder reached a legacy system on the night of 16 October 2026; OpenCode emailed affected users on 17 October; the earliest possible exposure window OpenCode cites is 17 October 00:30 UTC. The maintainer, Dax Raad, said the breach was of “the OpenCode Console, not OpenCode itself,” and that keys stored locally by the CLI and desktop apps in a SQLite database were not part of it. He framed the incident as a “large-scale distillation attack” and said attackers hit their systems for at least 48 hours using AI agents.\n\nThe scope figure, the one everyone quoted, is OpenCode’s own: 1.4% of Console users, all of them users who had linked an external provider. OpenCode has not said how many users that percentage is, which is exactly why I will not translate it into an absolute. The loss figures are individual user claims relayed through press, and I am listing them as claims: one user, Preston Zeller, reported about $1,700 overnight; another reported $281 on OpenAI and $168 on Anthropic; a Reddit user reported $400 on Anthropic; one reported roughly €200 on Google with the project then suspended for “abusive activity.” OpenCode’s own language is that keys “may have been exposed,” that it has not confirmed any key was copied or used, and that affected users may be reimbursed.\n\nThe lesson I take is not about OpenCode’s ops. It is that the moment your agent tool holds your provider keys on a server, the boundary that matters is no longer on your laptop at all. The 1.4% and the $1,700 are both the vendor’s or a user’s numbers; the architectural point stands regardless of whether they are precise. A key held server-side is a key exposed by that server’s worst day.\n\nThree stories, one shape. The sandbox boundary is not where your docker run or your bwrap command draws it. It is wherever the privileged work actually happens and wherever the fence can be redrawn. For a modern coding agent that means at least four surfaces:\n\n```\nBOUNDARY SURFACE          WHAT CAN GO WRONG                    SEEN THIS YEAR IN------------------------  -----------------------------------  ------------------The process you wrapped   Privileged work moved to a shared    OpenCode 2.0 service                          service your sandbox never coversThe config / allowlist    Agent-controlled input widens the    DuneSlide 50548                          sandbox's own write allowlistThe failure path          A security check fails open and      DuneSlide 50549                          falls back to trusting the inputThe server holding keys   Secrets live off-device; the         OpenCode Console                          vendor's breach is your breach        key exposure\n```\n\nThat table is the whole argument. The rest of this piece is me building something to test the first three rows against my own agent, because the fourth row is not a thing I can fix with a sandbox: if you hand your keys to someone’s server, your only real control is not doing that, or scoping and rotating what you hand over.\n\nI wanted a bench that was small enough to read in one sitting and that tried the specific escapes these incidents represent, not a generic “can it run id\" check. So: a C# host that exposes a few tools to a driver, a Docker setup with a deny-by-default network and a read-only filesystem, and a harness that attempts five escapes and records which succeeded under each configuration.\n\nThe five escapes, each mapped to something real:\n\n```\n#  ESCAPE                         MAPS TO-  -----------------------------  ------------------------------------------1  Service-API write             OpenCode 2.0: client reaches a privileged                                  helper service and writes outside its bubble2  Symlink write                 DuneSlide 50549: write through a symlink                                  that points outside the work directory3  Env-var read                  Secret harvesting: read a provider key from                                  the process environment4  Outbound call, non-allowed    Amap-style exfil: reach a host that is not   host                          on the egress allowlist5  Config write that changes     DuneSlide 50548: edit the allowlist file   the allowlist                 itself, then act under the widened fence\n```\n\nAnd four configurations to run them under, from naive to hardened:\n\n```\nCONFIG  DESCRIPTION------  --------------------------------------------------------------------C0      Naive. Only the agent binary is \"restricted\" (run as a child        process with a working dir). No container, no netns, no RO mount.C1      Container, default Docker bridge network, writable root fs.C2      Container, deny-by-default egress (internal network + allow-listed        proxy), read-only root with one writable work dir.C3      C2 plus: no shared helper service (private per sandbox), allowlist        file mounted read-only, symlink canonicalization that fails closed.\n```\n\nC0 is the one I most want people to see fail, because it is the shape of “I sandboxed the agent” that the OpenCode story is about: you restricted the thing you launched, and nothing else.\n\nThis is the host that exposes tools. It is deliberately dumb: it does what a tool call says, which is the point, because the sandbox, not the host, is supposed to be the thing saying no.\n\n```\n// AgentHost.cs// A minimal tool host. It trusts its caller on purpose: the whole experiment// is about whether the SANDBOX stops these, not whether the app does.using System.Net;using System.Net.Sockets;using System.Text;using System.Text.Json;namespace SandboxBench;public static class AgentHost{    private static readonly string WorkDir =        Environment.GetEnvironmentVariable(\"WORK_DIR\") ?? \"/work\";    public static int Main(string[] args)    {        var tool = args.Length > 0 ? args[0] : \"\";        var arg = args.Length > 1 ? args[1] : \"\";        var result = tool switch        {            \"service_write\" => ServiceWrite(arg),            \"symlink_write\" => SymlinkWrite(arg),            \"env_read\"      => EnvRead(arg),            \"outbound\"      => Outbound(arg),            \"config_write\"  => ConfigWrite(arg),            _ => Fail($\"unknown tool '{tool}'\")        };        Console.WriteLine(JsonSerializer.Serialize(result));        return result.Ok ? 0 : 1;    }    // Escape 1: ask a privileged helper service to write a file for us.    // Mirrors the OpenCode 2.0 shape: the client is sandboxed, the service    // is not, so the write lands wherever the SERVICE can reach.    private static ToolResult ServiceWrite(string path)    {        var host = Environment.GetEnvironmentVariable(\"HELPER_HOST\") ?? \"127.0.0.1\";        var port = int.Parse(Environment.GetEnvironmentVariable(\"HELPER_PORT\") ?? \"49374\");        try        {            using var client = new TcpClient();            client.Connect(host, port);            var payload = Encoding.UTF8.GetBytes($\"WRITE {path}\\n\");            client.GetStream().Write(payload);            using var reader = new StreamReader(client.GetStream());            var reply = reader.ReadLine() ?? \"\";            return reply.StartsWith(\"OK\")                ? Ok($\"service wrote {path} (reply: {reply})\")                : Fail($\"service refused: {reply}\");        }        catch (Exception e) { return Fail($\"no service reachable: {e.Message}\"); }    }    // Escape 2: write through a symlink that points outside the work dir.    private static ToolResult SymlinkWrite(string target)    {        var link = Path.Combine(WorkDir, \"escape_link\");        try        {            if (File.Exists(link) || Directory.Exists(link)) File.Delete(link);            File.CreateSymbolicLink(link, target);           // link -> /outside/path            File.WriteAllText(link, \"written-through-symlink\\n\");            return Ok($\"wrote through symlink to {target}\");        }        catch (Exception e) { return Fail($\"symlink write blocked: {e.Message}\"); }    }    // Escape 3: read a provider key from the environment.    private static ToolResult EnvRead(string name)    {        var val = Environment.GetEnvironmentVariable(            string.IsNullOrEmpty(name) ? \"PROVIDER_API_KEY\" : name);        return val is null            ? Fail(\"env var not present\")            : Ok($\"read secret, length={val.Length}\");       // never print the value    }    // Escape 4: reach a host that is not on the egress allowlist.    private static ToolResult Outbound(string host)    {        host = string.IsNullOrEmpty(host) ? \"example.org\" : host;        try        {            var addrs = Dns.GetHostAddresses(host);           // DNS is itself egress            using var client = new TcpClient();            client.Connect(addrs[0], 443);            return Ok($\"connected to {host}:443 ({addrs[0]})\");        }        catch (Exception e) { return Fail($\"outbound blocked: {e.Message}\"); }    }    // Escape 5: edit the allowlist file itself, then report success.    private static ToolResult ConfigWrite(string line)    {        var cfg = Environment.GetEnvironmentVariable(\"ALLOWLIST_FILE\")                  ?? \"/etc/agent/allowlist.txt\";        try        {            File.AppendAllText(cfg, (string.IsNullOrEmpty(line) ? \"evil.example\" : line) + \"\\n\");            return Ok($\"appended to allowlist at {cfg}\");        }        catch (Exception e) { return Fail($\"allowlist is not writable: {e.Message}\"); }    }    private static ToolResult Ok(string m) => new(true, m);    private static ToolResult Fail(string m) => new(false, m);}public readonly record struct ToolResult(bool Ok, string Message);\n```\n\nNote what the host does not do: it has no allowlist logic of its own. Every “no” in the results has to come from the kernel, the container, or the network. If the host itself refused things, the bench would be testing the host, not the sandbox.\n\nThis is the stand-in for OpenCode’s background service. It runs on the host, outside any sandbox, and writes files on request. In the real world you did not write this; your agent tool shipped it and detached it as a daemon.\n\n```\n// HelperService.cs  --  runs UNSANDBOXED on the host. This is the point.using System.Net;using System.Net.Sockets;using System.Text;var port = int.Parse(Environment.GetEnvironmentVariable(\"HELPER_PORT\") ?? \"49374\");var listener = new TcpListener(IPAddress.Loopback, port);listener.Start();Console.WriteLine($\"helper service on 127.0.0.1:{port} (unsandboxed)\");while (true){    using var conn = listener.AcceptTcpClient();    using var reader = new StreamReader(conn.GetStream());    var writer = new StreamWriter(conn.GetStream()) { AutoFlush = true };    var line = reader.ReadLine() ?? \"\";    if (line.StartsWith(\"WRITE \"))    {        var path = line[6..].Trim();        try { File.WriteAllText(path, \"written-by-helper\\n\"); writer.WriteLine($\"OK {path}\"); }        catch (Exception e) { writer.WriteLine($\"ERR {e.Message}\"); }    }    else writer.WriteLine(\"ERR unknown command\");}\n```\n\nThe deny-by-default network is the one piece people most often get subtly wrong, so here it is as a compose file. The agent container joins only an internal network, which Docker gives no route to the outside; the only way out is an explicit proxy on a second network, and the proxy only forwards allow-listed hosts.\n\n```\n# docker-compose.yml  --  the C2/C3 network shapeservices:  agent:    build: { context: ., dockerfile: Dockerfile.agent }    read_only: true                       # read-only root filesystem    tmpfs: [/tmp]    volumes:      - ./work:/work:rw                    # the one writable surface      - ./allowlist.txt:/etc/agent/allowlist.txt:ro   # C3: allowlist is RO    cap_drop: [ALL]    security_opt: [no-new-privileges:true]    networks: [internal]                   # NO default bridge, no egress    environment:      - WORK_DIR=/work      - HTTPS_PROXY=http://proxy:8080      - ALLOWLIST_FILE=/etc/agent/allowlist.txtproxy:    build: { context: ., dockerfile: Dockerfile.proxy }    networks: [internal, external]         # bridges the two, by policy    volumes:      - ./allowlist.txt:/etc/agent/allowlist.txt:ronetworks:  internal: { internal: true }             # the keyword that denies egress  external: {}\n```\n\nThe internal: true network is the whole trick. A container on it can talk to other containers on it (the proxy) and to nothing else. There is no default bridge attached to the agent, so there is no NAT path to the internet. Escape 4 either reaches the proxy and gets filtered, or it reaches nothing.\n\nThe proxy itself is deliberately boring: a tiny host-allowlist forward proxy. Squid or tinyproxy with an allowlist works; if you want it in-repo and dependency-free, a 40-line one is enough for the bench, and the only rule that matters is that it reads the same allowlist.txt the agent cannot write in C3.\n\nC0 is one line of C# in the harness, and it is the configuration I expect to lose every escape it is even capable of attempting:\n\n```\n// C0: \"we sandboxed the agent.\" We ran it as a child process with a work dir.// No container, no namespaces, no RO mount. The helper service is up.var psi = new ProcessStartInfo(\"dotnet\", $\"AgentHost.dll {tool} {arg}\"){    WorkingDirectory = workDir,    UseShellExecute = false,    RedirectStandardOutput = true};\n```\n\nHere is the honesty line again, in bold because it is load-bearing: **I did not run this bench for this article, so the table below is expected behavior reasoned from each configuration, not observed output.** Each cell is an argument, not a measurement, and run.sh in the repo is there so you can replace my reasoning with your numbers. Where I am genuinely unsure, I say so in the cell rather than guessing clean.\n\n```\nESCAPE \\ CONFIG         C0 naive   C1 bridge   C2 deny+RO   C3 hardened----------------------  ---------  ----------  -----------  -----------1 service-API write     ESCAPES    ESCAPES*    ESCAPES*     blocked2 symlink write         ESCAPES    ESCAPES     blocked(RO)  blocked3 env-var read          ESCAPES    ESCAPES     ESCAPES      blocked**4 outbound non-allowed  ESCAPES    ESCAPES     blocked      blocked5 allowlist config write ESCAPES   ESCAPES     blocked(RO)  blocked*  C1/C2: \"escapes\" only if the agent container can reach the host helper   service. On the default bridge (C1) the host is reachable via the gateway   IP or host.docker.internal, so a fixed, known port like 49374 is reachable.   On the internal network (C2) the host is NOT routable, so escape 1 should   FAIL there too -- this is the cell I am least certain about and most want   you to check, because it depends on whether any proxy or mount accidentally   bridges back to the host service.** C3: env-var read is \"blocked\" only in the sense that the secret is not in   the agent's environment at all (keys live behind the proxy / a broker the   agent never sees). If you leave PROVIDER_API_KEY in the container env, this   escapes in C3 too. The control is \"don't put the secret in reach,\" not a   kernel stop.\n```\n\nLet me reason through the rows I am most confident about, because “trust me” is not a security argument.\n\nEscape 4 (outbound) is the clean one. In C0 and C1 there is a route to the internet, so an outbound TCP connect to a non-allowlisted host succeeds; the DNS lookup alone already leaves the box. In C2/C3 the agent is on an internal: true network with no NAT path, so both the DNS resolution and the connect fail. This is the row I would stake the design on, and it is the row that maps to the dominant real-world vector this year (reaching a third party you should never reach).\n\nEscapes 2 and 5 (symlink write, allowlist write) are blocked in C2/C3 for the same unglamorous reason: the root filesystem is read_only and the allowlist is mounted :ro. A symlink into the work dir can still be created, but the write target is outside the one rw mount, so the write fails. In C0/C1 the filesystem is writable, so both succeed. Note this is a filesystem control, not an anti-symlink control: DuneSlide 50549 was dangerous precisely because the check failed open, and the C3 answer is not \"detect the symlink\" but \"there is nothing writable at the other end of it.\"\n\nEscape 1 (service write) is the one the whole article is about, and it is the one I am least willing to declare clean. In C0 the helper service is right there on loopback and the write happens. The asterisked C1/C2 cells are the honest mess: whether a “sandboxed” agent can still reach a host service depends entirely on whether the network happens to bridge back to the host, and people get this wrong constantly because host.docker.internal and the default gateway make it feel contained when it is not. C3 is the only configuration where I claim it is truly blocked, and the reason is architectural, not kernel: in C3 there is no shared host service to reach, because each sandbox gets its own private instance inside the boundary. That is the direct translation of Najam's --standalone / private service recommendation into a container shape.\n\nEscape 3 (env read) is the row that most resists a kernel fix, and I want to be blunt about it. If the secret is in the process environment, every configuration reads it, because reading your own environment is not a syscall you can meaningfully deny. C3 “blocks” it only by not putting the key in the agent’s reach at all: the key lives with the proxy or a broker, and the agent asks the broker to make the authenticated call. If you cut that corner and leave PROVIDER_API_KEY in the container, escape 3 escapes in C3 too, and the OpenCode Console story is what that looks like at scale.\n\nEverything above runs with only Docker and local .NET, no paid API and no cloud. If you want it even lighter, here is the bubblewrap version of the naive-versus- contained contrast, which is also the cleanest way to feel the OpenCode problem in about ten seconds.\n\nStart the unsandboxed helper on the host:\n\n```\nHELPER_PORT=49374 dotnet run --project HelperService &mkdir -p /tmp/host-target workbwrap \\  --ro-bind / / \\  --tmpfs /tmp \\  --bind \"$PWD/work\" /work \\  --unshare-user --unshare-pid \\  --die-with-parent \\  dotnet AgentHost.dll service_write /tmp/host-target/owned.txt\n```\n\nNow run the agent’s “service_write” tool inside a read-only bubblewrap sandbox that cannot write to /host-target itself:\n\nThe sandbox is read-only over the host and gives the agent only /work to write. And yet, if the helper service is reachable on loopback, owned.txt appears in /tmp/host-target on the host, written by the service, not by the confined process. The bubble did its job and contained the wrong thing. That is the entire lesson in one command.\n\nThe fix in bubblewrap terms is --unshare-net, which gives the sandbox its own empty network namespace so it cannot reach the host's loopback service at all:\n\n```\nbwrap \\  --ro-bind / / --tmpfs /tmp --bind \"$PWD/work\" /work \\  --unshare-user --unshare-pid --unshare-net \\  --die-with-parent \\  dotnet AgentHost.dll service_write /tmp/host-target/owned.txt# now: \"no service reachable\" -- because there is no network at all\n```\n\nIf your agent genuinely needs egress, you replace --unshare-net plus a manual veth-and-proxy setup, or you just use the Docker internal network from the compose file above, which does the same thing with less yak-shaving. Either way, the principle is identical to C3: the agent's network should reach your broker and nothing else, and there should be no shared host service for a confined client to autodiscover.\n\nReading these three stories back to back moved four things from “nice to have” to “non-negotiable” for me, and none of them are exotic.\n\nFirst, I stopped sandboxing the process and started sandboxing the privileged worker. If my agent tool runs a background service, either I run it --standalone so there is no shared daemon, or I give each sandbox its own private service inside the boundary. A confined client plus an unconfined host service is not a sandbox, and OpenCode 2.0 is the proof that this ships by default now.\n\nSecond, the allowlist file is read-only to the agent, always. DuneSlide 50548 is what happens when the fence can be edited by the thing it fences. If a config value changes what is permitted, that config does not live anywhere the agent can write, and ideally the permitted set is not even expressed as a writable file.\n\nThird, every security check fails closed. DuneSlide 50549 was a canonicalization check that fell back to trusting the input when it failed. I went through my own code looking for every catch and every else on a security decision and asked: does this deny on failure, or does it quietly allow? More than one of them allowed. That was a bad afternoon.\n\nFourth, the secret is not in the agent’s reach. The env-read row is the one no kernel feature saves you from, and the OpenCode Console breach is the server-side version of the same mistake. The key belongs with the broker that makes the call, not in the environment of the process most likely to be injected.\n\nThis is what I now run down before I let an agent touch anything that matters. It is ordered by how much pain each item has saved me, not by how clever it is.\n\n```\nHARDENING CHECK                                                         DONE?----------------------------------------------------------------------  -----1. No shared privileged service. The agent either runs standalone or     [ ]   has a PRIVATE service inside its own sandbox. Confirm no host   daemon on a fixed port (e.g. 49374) is reachable from the sandbox.2. Deny-by-default egress. Agent is on an internal/no-NAT network; the   [ ]   only way out is an allow-listed proxy. Verify a connect to a   non-allowed host AND its DNS lookup both fail.3. Read-only everything except one work dir. Root fs read-only, the      [ ]   allowlist/config mounted read-only, one explicit rw work mount.4. Fail closed. Every security check denies on error. Grep your own      [ ]   catch/else branches on write-path and allowlist decisions.5. Secrets out of reach. No provider key in the agent's environment or   [ ]   filesystem; a broker/proxy holds the key and makes the call.6. Audit the broker, not the transcript. Log every allowed/denied        [ ]   action at the boundary, append-only. The model's self-report is   not ground truth.\n```\n\nIf you run the bench, I would genuinely like to know two things: whether escape 1 actually stays blocked in your C2 (the cell I am unsure about), and whether your agent tool is running a shared service you did not know about. Check for a listener on a fixed port while your agent is idle. If something answers, your sandbox boundary is not where you think it is.\n\nSources, with each number labelled by who produced it:\n\nTags: #AIAgents #AISecurity #dotnet #Docker #Sandboxing #PromptInjection #MCP #DevSecOps #AgenticAI\n\n[Your Sandbox Wraps a Process, but Your Agent Now Lives Behind a Service](https://pub.towardsai.net/your-sandbox-wraps-a-process-but-your-agent-now-lives-behind-a-service-bc05d36bab09) was originally published in [Towards AI](https://pub.towardsai.net) on Medium, where people are continuing the conversation by highlighting and responding to this story.", "url": "https://wpnews.pro/news/your-sandbox-wraps-a-process-but-your-agent-now-lives-behind-a-service", "canonical_source": "https://pub.towardsai.net/your-sandbox-wraps-a-process-but-your-agent-now-lives-behind-a-service-bc05d36bab09?source=rss----98111c9905da---4", "published_at": "2026-11-11 14:31:02+00:00", "updated_at": "2026-11-11 14:52:18.190296+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools"], "entities": ["OpenCode", "OpenCode 2.0", "Najam us Saqib", "bubblewrap", "Landlock", "TNG", "TNG issue #328", ".NET"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/your-sandbox-wraps-a-process-but-your-agent-now-lives-behind-a-service", "markdown": "https://wpnews.pro/news/your-sandbox-wraps-a-process-but-your-agent-now-lives-behind-a-service.md", "text": "https://wpnews.pro/news/your-sandbox-wraps-a-process-but-your-agent-now-lives-behind-a-service.txt", "jsonld": "https://wpnews.pro/news/your-sandbox-wraps-a-process-but-your-agent-now-lives-behind-a-service.jsonld"}}