dsh-netguard

Overview

Read this first: what it does not do

It is not an egress firewall. It governs web_fetch and web_search. It does not see one byte sent by bash, run_code, a terminal session, an MCP server, 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, which this release does not govern at all.
  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 nothing is denied. 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.

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