dsh-netguard

Overview

Read this first: what it does not do

It is not an egress firewall. It governs the bytes web_fetch and web_search send, and it reads the text of a bash, pwsh or run_code argument before the child process or worker starts. It does not see one byte that child sends once it is running, and it does not see a terminal session, an MCP server’s own transport, or a delegated external agent. Governing those needs confinement at the sandbox or network layer, which this package does not provide.

It is not a containment boundary. It runs in-process, at the agent’s own uid. run_code executes model-authored TypeScript in a worker thread with fetch, node:net and node:child_process all reachable — verified by running a faithful reproduction of that worker’s construction; a real TCP connect left the process and execSync('id -u') returned the harness’s own uid. So:

Five paths stay open with a perfect allowlist.

  1. The model channel. The agent encodes a secret into its next prompt and it leaves over the one connection the deployment must permit. This is the dominant path and nothing here touches it.
  2. Allowlisted destinations. github.com on the allow list is the whole GitHub API as whoever gh is authenticated as: push to any writable repository, a gist, an issue body. It is the widest entry you can add — a path-scoped entry such as github.com/your-org/your-repo narrows it, and narrows nothing else. Even inside one granted path, a request needs no response to carry data out: which URLs an agent asks for is itself a channel, which is what the distinct-URL signal watches for and does not close.
  3. run_code and bash, past the text of the argument. The command arm decides the destinations a command writes out; it decides nothing about curl "$(cat url.txt)", a socket opened to an address the program computes, or anything a child process does after it starts.
  4. The browser. DSH’s web UI renders model-authored markdown images from any absolute http(s) URL, with no CSP anywhere in the repo, and that request is made by your browser, not by the agent’s process.
  5. Search results on a stock profile. The guarded search provider wraps a vendor provider named in search.delegate. Without one it reports itself unusable — which is what keeps this plugin from breaking a profile’s existing search route — and the vendor the seam selected answers the seam directly. Only the outbound query is filtered; the result URLs reach the model unfiltered.

What it is good for, stated as narrowly as it is true:


Audit mode is the default, and audit mode is not a control

mode defaults to audit. In audit mode no host policy denies anything. Every decision is recorded and every request goes through, including the ones the policy would refuse. An installation left in audit mode has monitoring, not enforcement.

Two refusals are not the host policy’s and do not relax with it: a redirect that leaves the origin, and one that exceeds the hop budget. Those are the shipped fetch provider’s own behaviour, which this one replaces and preserves, so an allowlisted host cannot be used as an open redirector even here. Enforcement says why.

It is the default because enforce-first on a dependency graph nobody can enumerate in advance gets the control switched off in week one. The value of audit mode is that it writes the allow list for you:

dsh-netguard report --suggest     # a ready `allow:` block from the hosts it observed

Read that output before using it. It reports what happened, not what should be permitted, and one line in it may be the request you mounted this plugin to stop. When the list is right, set mode: enforce.


Next