Skip to content

fix: use wildcard in allowLocalBinding seatbelt rules for IPv6 dual-stack compatibility#127

Merged
dylan-conway merged 2 commits intomainfrom
dylanc/allow-local-binding-ipv6-dual-stack
Feb 10, 2026
Merged

fix: use wildcard in allowLocalBinding seatbelt rules for IPv6 dual-stack compatibility#127
dylan-conway merged 2 commits intomainfrom
dylanc/allow-local-binding-ipv6-dual-stack

Conversation

@dylan-conway
Copy link
Collaborator

@dylan-conway dylan-conway commented Feb 9, 2026

Problem

Gradle builds fail in sandbox mode with allowLocalBinding: true because the Gradle daemon cannot bind to TCP localhost:

java.net.SocketException: Operation not permitted
    at sun.nio.ch.Net.bind0(Native Method)
    at TcpIncomingConnector.accept()

Root Cause

Modern Java (and other runtimes) create IPv6 dual-stack sockets by default via ServerSocketChannel.open(). When binding such a socket to 127.0.0.1, the kernel internally represents the address as ::ffff:127.0.0.1 (IPv4-mapped IPv6 address).

macOS Seatbelt's (local ip "localhost:*") filter resolves localhost to 127.0.0.1 and ::1, but does not match ::ffff:127.0.0.1. Seatbelt only supports two host values in IP filters: localhost and * — there is no way to specify ::ffff:127.0.0.1 explicitly.

Socket type Bind address Seatbelt sees Matches localhost?
AF_INET (Python, explicit Java) 127.0.0.1 127.0.0.1 Yes
AF_INET6 (Java default) 127.0.0.1 ::ffff:127.0.0.1 No
AF_INET6 ::1 ::1 Yes

Fix

Change the seatbelt rules from (local ip "localhost:*") to (local ip "*:*").

Safety Analysis

Tested via sandbox-exec(local ip "localhost:*") is already more permissive than its name suggests. It allows binding, listening, inbound, and outbound on all local addresses, not just loopback:

Operation localhost:* *:*
Bind/listen 127.0.0.1 Allowed Allowed
Bind/listen 0.0.0.0 Allowed Allowed
Bind/listen LAN IP Allowed Allowed
Bind/listen ::1 / :: Allowed Allowed
Outbound TCP to internet Allowed Allowed
Accept inbound from LAN Allowed Allowed
Bind ::ffff:127.0.0.1 Blocked Allowed
Bind ::ffff:<LAN_IP> Blocked Allowed

The only new addresses *:* permits are the ::ffff:x.x.x.x (IPv4-mapped IPv6) family. These are equivalent to their unmapped counterparts — every operation achievable via ::ffff:<addr> can already be done via <addr> directly under localhost:*. The change grants no new network capability.

Verified via sandbox-exec testing:

  • Java dual-stack IPC (bind + connect + accept): works
  • Internet access: blocked
  • Python IPv4 IPC: works
  • Without allowLocalBinding: blocked

Fixes: anthropics/claude-code#18545

…tack compatibility

Modern runtimes like Java create IPv6 dual-stack sockets by default.
When binding such a socket to 127.0.0.1, the kernel represents the
address as ::ffff:127.0.0.1 (IPv4-mapped IPv6). macOS Seatbelt's
"localhost" filter only matches 127.0.0.1 and ::1, not the
IPv4-mapped variant, causing bind() to fail with EPERM.

Seatbelt only supports two host values in IP filters: "localhost"
and "*". Since we can't specify ::ffff:127.0.0.1 explicitly, change
to (local ip "*:*"). This is safe because the (local ip) filter
matches the LOCAL endpoint of connections — internet-bound traffic
originates from non-loopback interfaces, so it remains blocked by
the (deny default) rule.

Fixes: anthropics/claude-code#18545
Copy link
Collaborator

@ddworken ddworken left a comment

Choose a reason for hiding this comment

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

If it is practical, it would be cool to add some tests for this!

@dylan-conway dylan-conway merged commit 96800ee into main Feb 10, 2026
8 checks passed
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.

[BUG] Sandbox allowLocalBinding: true not applied to child/grandchild processes (breaks Gradle)

2 participants