Skip to content

feat: sandboxing for macos#7197

Merged
michaelneale merged 7 commits intomainfrom
micn/sandbox-impl
Feb 16, 2026
Merged

feat: sandboxing for macos#7197
michaelneale merged 7 commits intomainfrom
micn/sandbox-impl

Conversation

@michaelneale
Copy link
Collaborator

@michaelneale michaelneale commented Feb 13, 2026

This uses seatbelt/sandbox to optionally enforce access.

This uses the approach shown here: https://github.com/michaelneale/agent-seatbelt-sandbox which uses a proven .db tool and a proxy on macos so you can limit things like: goose editing its own config, track or limit what urls are accessed etc. Has no overhead and works with any tools.

cc @shellz-n-stuff

@michaelneale michaelneale self-assigned this Feb 13, 2026
Copy link
Collaborator

@shellz-n-stuff shellz-n-stuff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tested nicely on the security aspects. Obviously not a perfect sandbox but a great starting point that's lightweight and we can improve

}

export function isSandboxAvailable(): boolean {
return process.platform === 'darwin' && fs.existsSync('/usr/bin/sandbox-exec');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! Was looking for this

@michaelneale michaelneale changed the title feat: first pass at real sandboxing for macos feat: sandboxing for macos Feb 13, 2026
@michaelneale michaelneale marked this pull request as ready for review February 13, 2026 02:10
Copilot AI review requested due to automatic review settings February 13, 2026 02:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements macOS sandboxing for the goosed process using Apple's Seatbelt/Sandbox framework. The implementation blocks direct network access from the sandboxed process and routes all traffic through an HTTP/HTTPS CONNECT proxy running in the Electron main process. The proxy provides logging, domain blocking via a local blocklist, and optional LaunchDarkly-based egress control.

Changes:

  • Added sandbox profile (sandbox.sb) that blocks direct network access and file write access to sensitive config files
  • Implemented HTTP/HTTPS CONNECT proxy with domain blocking and LaunchDarkly integration
  • Integrated sandbox execution into goosed startup flow when GOOSE_SANDBOX=true

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
ui/desktop/src/sandbox/sandbox.sb Seatbelt profile that blocks direct network and protects config files
ui/desktop/src/sandbox/proxy.ts HTTP CONNECT proxy with blocklist and LaunchDarkly egress control
ui/desktop/src/sandbox/index.ts Sandbox initialization and proxy lifecycle management
ui/desktop/src/sandbox/blocked.txt Template blocklist file for domain blocking
ui/desktop/src/goosed.ts Integration of sandbox execution wrapper into goosed startup
ui/desktop/forge.config.ts Added sandbox directory to packaged resources

Copilot AI review requested due to automatic review settings February 13, 2026 04:55
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Comment on lines +67 to +73
lines.push(
'',
'(deny network*)',
'(allow network-outbound (literal "/private/var/run/mDNSResponder"))',
'(allow network-outbound (remote unix-socket))',
'(allow network-outbound (remote ip "localhost:*"))',
'(allow network-inbound (local ip "localhost:*"))'
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowing outbound access to "localhost:*" means the sandboxed process can talk to any local service (including user-run local proxies/tunnels) and potentially reach the internet without going through this proxy; if the goal is to force all egress through the proxy, consider restricting outbound loopback to the proxy port (and optionally an explicit local-port allowlist) or clarify the intended threat model in the profile/docs.

Suggested change
lines.push(
'',
'(deny network*)',
'(allow network-outbound (literal "/private/var/run/mDNSResponder"))',
'(allow network-outbound (remote unix-socket))',
'(allow network-outbound (remote ip "localhost:*"))',
'(allow network-inbound (local ip "localhost:*"))'
const proxyPort = process.env.GOOSE_SANDBOX_PROXY_PORT;
const loopbackIpPattern =
proxyPort && /^\d+$/.test(proxyPort) ? `localhost:${proxyPort}` : 'localhost:*';
lines.push(
'',
'(deny network*)',
'(allow network-outbound (literal "/private/var/run/mDNSResponder"))',
'(allow network-outbound (remote unix-socket))',
`(allow network-outbound (remote ip "${loopbackIpPattern}"))`,
`(allow network-inbound (local ip "${loopbackIpPattern}"))`

Copilot uses AI. Check for mistakes.
Adds a macOS sandbox for the goosed process using seatbelt (sandbox-exec)
and an HTTP CONNECT proxy for egress filtering. All outbound network
traffic from goosed is forced through a local proxy that enforces domain
blocking, IP restrictions, and SSH allowlisting.

## Sandbox profile (seatbelt)

The sandbox profile is built programmatically via buildSandboxProfile()
rather than a static template, ensuring it is always regenerated fresh
on each spawn and never becomes stale across app updates.

Configurable protections (all default to enabled):
- GOOSE_SANDBOX_PROTECT_FILES: write-protect ~/.ssh, shell configs
- GOOSE_SANDBOX_BLOCK_RAW_SOCKETS: deny SOCK_RAW on AF_INET/AF_INET6
- GOOSE_SANDBOX_BLOCK_TUNNELING: block nc, ncat, netcat, socat, telnet

Always-on protections:
- All network denied except localhost (forces proxy usage)
- Sandbox config and goose config are write-protected
- Kernel extension loading denied

## Egress proxy

An HTTP/CONNECT proxy runs in the Electron main process on 127.0.0.1.
The sandboxed process uses it via HTTP_PROXY/HTTPS_PROXY env vars.

Blocking layers checked in order:
1. Loopback detection (prevents proxy-as-relay bypass)
2. Raw IP address blocking (no domain = blocked)
3. Local blocklist (blocked.txt, live-reloaded via fs.watch)
4. SSH/Git host restriction (port 22/2222/7999)
5. LaunchDarkly egress-allowlist flag (optional, with configurable failover)

Configurable proxy options:
- GOOSE_SANDBOX_ALLOW_IP: allow raw IP connections (default: blocked)
- GOOSE_SANDBOX_BLOCK_LOOPBACK: block loopback relay via proxy (default: off)
- GOOSE_SANDBOX_ALLOW_SSH: enable/disable SSH (default: on)
- GOOSE_SANDBOX_GIT_HOSTS: custom git host allowlist for SSH
- GOOSE_SANDBOX_SSH_ALL_HOSTS: allow SSH to any host (default: off)
- GOOSE_SANDBOX_LD_FAILOVER: LaunchDarkly failover mode (allow|deny|blocklist)

## Other changes

- Startup now throws an error if GOOSE_SANDBOX=true but sandbox-exec is
  unavailable, instead of silently proceeding unsandboxed
- Domain normalization handles trailing dots, punycode, and IPv6 brackets
- Blocklist loaded once at startup and cached in memory with fs.watch
  for live reload (no per-request disk I/O)
- Proxy resources cleaned up on app quit
- Removed unused dns import and resolveIPToHostnames function
- Formatted with prettier

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019c551f-ba60-709e-b293-5dedad8d243f
@@ -0,0 +1,43 @@
#!/usr/bin/perl
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh, perl?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct reaction. Mic and I were talking a bit about this, to proxy git you need to give it a "proxy command".

I couldn't get it working in Bash and we know Perl is available on MacOS machines by default (system internal usage). The alternatives we spoke about (and also happy to use) were:

  1. Spawn a Node server JIT (I opted against this as it seemed non-performant).
  2. Have another rust binary we produce and then bundle as part of this.

I'd be super open to other/better ideas.

It's worth noting this was actually blocked by Mic's initial commit not my additional self-ssh mitigations so for any for of network sandboxing (without just general bypass over port 22) we would need something shaped like this to allow Git over SSH (or just enforce HTTP only but that has CorpEnv implications for Block and likely others who may want to try this out)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah - no other obvious solution yet... this was my reaction and still is!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DOsinga @shellz-n-stuff weirdly it does seem the idiomatic solution for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and this is only for ssh, https will work the usual way. I think I have made peace with this approach.

Copy link
Collaborator

@DOsinga DOsinga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

// ---------------------------------------------------------------------------

export function isSandboxEnabled(): boolean {
return process.env.GOOSE_SANDBOX === 'true' || process.env.GOOSE_SANDBOX === '1';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should maybe also check here if we are on macos since the current implementation only works there

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked here:

We check if the user has requested (via env vars) then if it's actually available to provide)

Copilot AI review requested due to automatic review settings February 16, 2026 00:48
@michaelneale michaelneale requested a review from a team as a code owner February 16, 2026 00:48
@github-actions
Copy link
Contributor

github-actions bot commented Feb 16, 2026

PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-02-16 22:16 UTC

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 11 comments.

Copilot AI review requested due to automatic review settings February 16, 2026 01:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.

Copy link
Collaborator

@shellz-n-stuff shellz-n-stuff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, tested again git working nicely. Blocked domains.

Raw IPs however are working as a bypass again

@shellz-n-stuff
Copy link
Collaborator

LGTM, tested again git working nicely. Blocked domains.

Raw IPs however are working as a bypass again

Retested this works fine. It was a local config issue

@michaelneale michaelneale added this pull request to the merge queue Feb 16, 2026
Merged via the queue into main with commit 1ad641c Feb 16, 2026
28 of 29 checks passed
@michaelneale michaelneale deleted the micn/sandbox-impl branch February 16, 2026 22:10
michaelneale added a commit that referenced this pull request Feb 17, 2026
* main:
  fix: handle reasoning_content for Kimi/thinking models (#7252)
  feat: sandboxing for macos (#7197)
  fix(otel): use monotonic_counter prefix and support temporality env var (#7234)
  Streaming markdown (#7233)
  Improve compaction messages to enable better post-compaction agent behavior (#7259)
  fix: avoid shell-escaping special characters except quotes (#7242)
tlongwell-block added a commit that referenced this pull request Feb 17, 2026
* origin/main:
  docs: playwright CLI skill tutorial (#7261)
  install node in goose dir (#7220)
  fix: relax test_basic_response assertion for providers returning reasoning_content (#7249)
  fix: handle reasoning_content for Kimi/thinking models (#7252)
  feat: sandboxing for macos (#7197)
  fix(otel): use monotonic_counter prefix and support temporality env var (#7234)
  Streaming markdown (#7233)
  Improve compaction messages to enable better post-compaction agent behavior (#7259)
  fix: avoid shell-escaping special characters except quotes (#7242)
  fix: use dynamic port for Tetrate auth callback server (#7228)
  docs: removing LLM Usage admonitions (#7227)
  feat(otel): respect standard OTel env vars for exporter selection (#7144)
  fix: fork session (#7219)
  Bump version numbers for 1.24.0 release (#7214)
  Move platform extensions into their own folder (#7210)
  fix: ignore deprecated skills extension (#7139)

# Conflicts:
#	Cargo.lock
#	Cargo.toml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants