[BUG] macOS sandbox: System V shared-memory and sysctl calls are refused with EPERM whenever two sandboxed Bash commands run at the same time — and a refused shmget/shmctl still leaves the segment allocated

Status Open
Reported on v2.1.258
Maintainer reply None cached
Activity 1 comment · opened Sep 2, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

A single sandboxed Bash command can call shmget, shmctl(IPC_RMID) and run ipcs -mo (which uses sysctlbyname) as often as it likes: 40 calls, 0 refusals.

The moment a second sandboxed Bash command is running at the same time — a run_in_background Bash call, a subagent's Bash call, or another Claude Code session on the same machine — the same calls are refused with EPERM ("Operation not permitted") in both commands, for the whole time they overlap. Reproduced three times: 40/40 allowed
alone, 0/40 during overlap.

Two consequences make this worse than a refusal:

  1. A refused create still allocates the segment. PostgreSQL's initdb failed with FATAL: could not create shared memory segment: Operation not permitted / DETAIL: Failed system call was shmget(key=12353002, size=56, 03600) — and afterwards ipcs -mo showed a segment with key 0x00bc7dea (= 12353002), NATTCH 0, that nothing had created except that refused call. Two refused starts left two orphans with exactly the keys named in the two FATAL lines.
  2. A refused remove leaves the segment behind. A loop that created a private segment and removed it 40 times, run in two shells at once, left 29 orphan segments (every shmctl(IPC_RMID) was refused), after which every shmget on the machine returned ENOSPC and no PostgreSQL could start — the user's own live server included, until a person removed the orphans from a normal terminal.

So on a machine that runs PostgreSQL-backed tests from more than one agent, the sandbox silently exhausts a scarce kernel resource (32 segments for the whole machine) within minutes.

What Should Happen?

The sandbox's decision for a system call should not depend on whether another sandboxed command happens to be running.
Either the calls are allowed (the profile does not mention System V IPC) or they are denied consistently — and a denied
shmget should not leave an allocated segment.

Error Messages/Logs

Steps to Reproduce

Minimal reproduction

shm_loop.py — creates and removes a private segment 40 times, 0.3 s apart, and reports refusals:

import ctypes, ctypes.util, os, errno, time, subprocess
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
ok = refused = 0
for i in range(40):
    shmid = libc.shmget(0, 4096, 0o1000 | 0o600)          # IPC_PRIVATE, IPC_CREAT | 0600
    if shmid < 0:
        e = ctypes.get_errno(); refused += 1
        ipcs = subprocess.run(["/usr/bin/ipcs", "-mo"], capture_output=True, text=True).returncode
        print(f"{time.strftime('%H:%M:%S')} shmget REFUSED {errno.errorcode.get(e)}  ipcs_exit={ipcs}")
    else:
        if libc.shmctl(shmid, 0, None) < 0:                  # IPC_RMID
            e = ctypes.get_errno(); print(f"shmctl RMID REFUSED {errno.errorcode.get(e)} -- segment {shmid} ORPHANED")
        ok += 1
    time.sleep(0.3)
print(f"ok={ok} refused={refused}")

ipcs_loop.sh — the listing only, no segment is ever created, so this one cannot leak:

#!/bin/zsh
ok=0; refused=0
for i in {1..40}; do
  if /usr/bin/ipcs -mo >/dev/null 2>&1; then ok=$((ok+1)); else refused=$((refused+1)); echo "$(date +%H:%M:%S) ipcs REFUSED"; fi
  sleep 0.3
done
echo "ok=$ok refused=$refused"

Steps, inside Claude Code with the sandbox enabled:

  1. Run python3 shm_loop.py as one Bash tool call. Result: ok=40 refused=0.
  2. Run ./ipcs_loop.sh as a Bash tool call with run_in_background: true, and in the same turn run ./ipcs_loop.sh as a foreground Bash tool call. Result in both: ok=0 refused=40, every line ipcs REFUSED.
  3. (Leaks — clean up afterwards with ipcrm -m <id> on the NATTCH-0 segments from a normal terminal.) Run python3 shm_loop.py in the background and again in the foreground at the same time. Result: shmctl RMID REFUSED EPERM on every created segment, then shmget REFUSED ENOSPC once the 32 slots are full; ipcs -mo afterwards lists ~29 segments with key 0x00000000 and NATTCH 0.
  4. For the PostgreSQL shape: run initdb -D /tmp/x inside a sandboxed Bash call while another sandboxed Bash call is running. initdb fails with the FATAL line above, and ipcs -mo afterwards shows a NATTCH-0 segment whose key is the one the FATAL line named.

Timestamps from the measured runs: refusals at 08:56:17–08:56:19 and 08:58:46–08:58:54 (concurrent), allowed at 08:55:57, 08:56:08, 08:56:35–08:56:41 (alone) and a further 09:10 solo run at 40/40 followed by a concurrent run at 0/40.

Claude Model

Other

Is this a regression?

No, this never worked

Last Working Version

_No response_

Claude Code Version

2.1.258

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

Workaround in use

sandbox.excludedCommands naming the test command ("*venv/bin/python -m pytest *"), which runs PostgreSQL-backed
tests outside the sandbox; plus a test fixture that records every segment it creates or is refused on, retries a refused
start, and removes its own segments by identifier with the removal's return value checked — because ipcs is refused in
the same window, a cleanup that depends on the listing cannot help.

Notes for the maintainers

  • ipcs failing looks exactly like an empty machine: it prints its header row and an empty table and exits 1 with

ipcs: sysctlbyname: Operation not permitted on stderr. Tooling that reads a failed listing as "no segments" will make
wrong decisions.

  • The refusal windows were not always short. In one session, running one command at a time, ipcs and single-segment

scripts were refused for about 17 minutes with brief gaps while a second Claude Code window was open on the same machine.

Environment

| | |
|---|---|
| Claude Model | Fable 5.1 |
| Claude Code | 2.1.258 |
| macOS | 26.5.2 (build 25F84), Darwin 25.5.0, arm64 |
| Sandbox settings (user) | "sandbox": {"allowUnsandboxedCommands": false, "strictAllowlist": true, "failIfUnavailable": true, "filesystem": {"disabled": true}} |
| Sandbox settings (project) | "sandbox": {"enabled": true, "network": {"deniedDomains": [...], "allowLocalBinding": true}, ...} |
| Affected program | PostgreSQL 18.4 (Homebrew) — initdb and postmaster need one System V segment each for the data-directory interlock |
| sysctl kern.sysv.shmmni | 32 (the machine-wide limit on segments) |

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗