From cb99c918aa2f4378581808aa700ac7be2b7452c9 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 27 Jul 2026 00:27:21 +0900 Subject: [PATCH 1/2] Document Claude Code sandbox EPERM on Unity IPC and its command-text exclusion boundary A sandboxed agent shell denies Unix domain socket connect/bind with EPERM, which the CLI currently misreports as a retryable i/o timeout; one full investigation (2026-07-26) chased this as an IPC bug while the Editor was healthy the whole time. The sandbox exclusion that saves game projects matches on the typed command text (`uloop *`), so this repository's dev-binary rule (`dist//uloop`) is exactly the shape it does not cover. Record the verified invocation matrix, the remedies, and a pointer from the dev-binary validation section in AGENTS.md so the next agent reads this before re-investigating the Editor side. --- AGENTS.md | 5 +++ docs/claude-code-sandbox.md | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 docs/claude-code-sandbox.md diff --git a/AGENTS.md b/AGENTS.md index 45301ec4a2..7e36859340 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,6 +128,11 @@ dist/darwin-arm64/uloop compile --project-path "$(git rev-parse --show-toplevel) Substitute the binary for your platform (e.g. `dist/windows-amd64/uloop.exe` on Windows). +When an AI agent runs these dev-binary commands through a sandboxed shell, Unity IPC over the +Unix socket is blocked with EPERM (misreported as an i/o timeout) even though plain `uloop ...` +may appear to work. Before investigating any "Unity not reachable" symptom in that setting, +read `docs/claude-code-sandbox.md`. + Before running a command with `--project-path`, confirm the path is the intended Unity project for the current task — do not copy a sibling checkout path from another repository or session. diff --git a/docs/claude-code-sandbox.md b/docs/claude-code-sandbox.md new file mode 100644 index 0000000000..ecced9105a --- /dev/null +++ b/docs/claude-code-sandbox.md @@ -0,0 +1,66 @@ +# Claude Code Sandbox and Unity IPC + +Read this when a `uloop` command fails against a running, healthy Unity Editor while an AI agent +(Claude Code or similar) is executing it through a sandboxed shell. The symptom looks like an IPC +bug and has already cost one full investigation (2026-07-26) that ended in "the Editor was fine +all along" — this document exists so nobody walks that path again. + +## Symptom + +- Any `uloop` command that talks to the Unity Editor (`compile`, `run-tests`, `get-logs`, + `simulate-*`, ...) fails, while Unity itself is demonstrably healthy and the server-side log + shows a successful bind and then silence. +- Commands that never touch the Editor (`uloop --version`, `--help` rendering) work normally. +- The reported error is misleading: the CLI retries for 60 seconds and then reports + `dial unix ...: i/o timeout`, while the underlying per-attempt error is + `connect: operation not permitted` (EPERM). Fixing that misdiagnosis is tracked as its own + work item (report the permanent errno verbatim and stop advising retries). + +## Cause + +Claude Code runs shell commands inside a sandbox whose network policy is expressed as a list of +allowed **hostnames**. A Unix domain socket has no hostname, so there is no way to allowlist the +project socket (`/tmp/uloop-/.sock`) through that policy — `connect()` and +`bind()` on Unix sockets are denied with EPERM regardless of filesystem permissions. Write +access to the socket's directory does not help; this was verified empirically: a directory the +sandbox allowed file writes into still refused a socket `bind()`. + +This is specific to the sandboxed shell. The same command in a normal terminal, or in a session +without sandboxing, is unaffected. + +## Why this repository gets hit harder than game projects + +Claude Code's sandbox supports an `excludedCommands` list (personal `settings.json`), and a +typical entry is `"uloop *"`. That pattern matches the **command text**, with these verified +consequences (2026-07-26, all measured in a live sandboxed session): + +| Invocation | Matches `uloop *` | Result | +|---|---|---| +| `uloop get-logs ...` (dispatcher from PATH) | yes — runs excluded from the sandbox | works | +| `SOME_VAR=... uloop ...` (env-var prefix) | yes (verified empirically) | works | +| `ULOOP_PROJECT_RUNNER_PATH= uloop ...` (or `export` first, then plain `uloop ...`) | yes — the command text still starts with `uloop` | works | +| `dist/darwin-arm64/uloop compile ...` | **no** (verified even in the plain form with no `$(...)` substitution) | EPERM | +| raw `socket.connect()` from a script | no | EPERM | + +The exclusion is decided on the **typed command text**, not on which binary ultimately does the +work: the `ULOOP_PROJECT_RUNNER_PATH` row runs a locally built dev runner yet stays excluded, +while the `dist/...` row is the same dispatcher code yet gets sandboxed. Game projects invoke +plain `uloop ...` (optionally with the env override) and never notice the sandbox. This +repository's development rule (see `CLAUDE.md` — always validate with the built +`dist//uloop` binary) produces exactly the command shape the exclusion does +**not** match. + +Note the corollary: a success for `uloop ...` in a sandboxed session does not mean the sandbox +permits Unity IPC — it means the command was excluded from sandboxing entirely. + +## Remedies + +Pick one: + +1. Run dev-binary commands with the sandbox disabled for that command (Claude Code: + `dangerouslyDisableSandbox`; users can manage restrictions via `/sandbox`). +2. Add the dev-binary shape to `excludedCommands` in the personal Claude Code settings, e.g. + `"dist/*/uloop *"`, alongside the existing `"uloop *"`. + +Do not burn time re-investigating the Editor side while the error is EPERM: the Editor never +saw the connection attempt. From 8199153d8c3bf3765f0e2dd516268be0b6409681 Mon Sep 17 00:00:00 2001 From: Masamichi Hatayama Date: Mon, 27 Jul 2026 00:36:33 +0900 Subject: [PATCH 2/2] Correct the socket path format and add the runner-override remedy Blind review of #2015: the documented socket filename did not match createEndpointName (UnityCliLoop-, not the project name), the symptom bullet implied server bind logs exist without ULOOP_DEBUG, and the remedies omitted the ULOOP_PROJECT_RUNNER_PATH route the invocation table itself measures as working. Also mark the suggested excludedCommands glob as unverified, add the Windows named-pipe open question, and fix five awkward English spots. --- docs/claude-code-sandbox.md | 38 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/docs/claude-code-sandbox.md b/docs/claude-code-sandbox.md index ecced9105a..7caada7536 100644 --- a/docs/claude-code-sandbox.md +++ b/docs/claude-code-sandbox.md @@ -8,9 +8,10 @@ all along" — this document exists so nobody walks that path again. ## Symptom - Any `uloop` command that talks to the Unity Editor (`compile`, `run-tests`, `get-logs`, - `simulate-*`, ...) fails, while Unity itself is demonstrably healthy and the server-side log - shows a successful bind and then silence. -- Commands that never touch the Editor (`uloop --version`, `--help` rendering) work normally. + `simulate-*`, ...) fails, while Unity itself is demonstrably healthy and the server side never + sees the connection attempt (server-side logs are VibeLogger-based and exist only when the + `ULOOP_DEBUG` scripting define is set — do not read missing log lines as evidence either way). +- Commands that never touch the Editor (`uloop --version`, `uloop --help`) work normally. - The reported error is misleading: the CLI retries for 60 seconds and then reports `dial unix ...: i/o timeout`, while the underlying per-attempt error is `connect: operation not permitted` (EPERM). Fixing that misdiagnosis is tracked as its own @@ -20,13 +21,16 @@ all along" — this document exists so nobody walks that path again. Claude Code runs shell commands inside a sandbox whose network policy is expressed as a list of allowed **hostnames**. A Unix domain socket has no hostname, so there is no way to allowlist the -project socket (`/tmp/uloop-/.sock`) through that policy — `connect()` and +project socket (`/tmp/uloop-/UnityCliLoop-.sock`, where `` is the first 16 hex +digits of the SHA-256 of the canonical project root) through that policy — `connect()` and `bind()` on Unix sockets are denied with EPERM regardless of filesystem permissions. Write access to the socket's directory does not help; this was verified empirically: a directory the sandbox allowed file writes into still refused a socket `bind()`. This is specific to the sandboxed shell. The same command in a normal terminal, or in a session -without sandboxing, is unaffected. +without sandboxing, is unaffected. Windows uses a named pipe instead of a Unix socket; whether +the sandbox blocks named-pipe connects the same way has not been verified — treat an +EPERM-shaped failure there with the same suspicion before blaming the Editor. ## Why this repository gets hit harder than game projects @@ -36,22 +40,22 @@ consequences (2026-07-26, all measured in a live sandboxed session): | Invocation | Matches `uloop *` | Result | |---|---|---| -| `uloop get-logs ...` (dispatcher from PATH) | yes — runs excluded from the sandbox | works | +| `uloop get-logs ...` (dispatcher from PATH) | yes — runs outside the sandbox | works | | `SOME_VAR=... uloop ...` (env-var prefix) | yes (verified empirically) | works | | `ULOOP_PROJECT_RUNNER_PATH= uloop ...` (or `export` first, then plain `uloop ...`) | yes — the command text still starts with `uloop` | works | -| `dist/darwin-arm64/uloop compile ...` | **no** (verified even in the plain form with no `$(...)` substitution) | EPERM | +| `dist/darwin-arm64/uloop compile ...` | **no** (verified with the plain literal path) | EPERM | | raw `socket.connect()` from a script | no | EPERM | The exclusion is decided on the **typed command text**, not on which binary ultimately does the work: the `ULOOP_PROJECT_RUNNER_PATH` row runs a locally built dev runner yet stays excluded, -while the `dist/...` row is the same dispatcher code yet gets sandboxed. Game projects invoke -plain `uloop ...` (optionally with the env override) and never notice the sandbox. This -repository's development rule (see `CLAUDE.md` — always validate with the built +while the `dist/...` row runs the same kind of dispatcher binary yet gets sandboxed. Game +projects invoke plain `uloop ...` (optionally with the env override) and never notice the +sandbox. This repository's development rule (see `CLAUDE.md` — always validate with the built `dist//uloop` binary) produces exactly the command shape the exclusion does **not** match. -Note the corollary: a success for `uloop ...` in a sandboxed session does not mean the sandbox -permits Unity IPC — it means the command was excluded from sandboxing entirely. +Note the corollary: a successful `uloop ...` command in a sandboxed session does not mean the +sandbox permits Unity IPC — it means the command was excluded from sandboxing entirely. ## Remedies @@ -60,7 +64,13 @@ Pick one: 1. Run dev-binary commands with the sandbox disabled for that command (Claude Code: `dangerouslyDisableSandbox`; users can manage restrictions via `/sandbox`). 2. Add the dev-binary shape to `excludedCommands` in the personal Claude Code settings, e.g. - `"dist/*/uloop *"`, alongside the existing `"uloop *"`. + `"dist/*/uloop *"`, alongside the existing `"uloop *"` (this exact glob is a suggestion, not + a measured entry — confirm it matches after adding it). +3. When the change under review lives in the project runner, keep the sandbox on and run + `ULOOP_PROJECT_RUNNER_PATH= uloop ...` — the plain-`uloop` + command text stays excluded while the dev runner does the work (the override is documented in + `docs/project-runner-pin.md`). This does not exercise dispatcher-side changes; for those, use + remedy 1 or 2. -Do not burn time re-investigating the Editor side while the error is EPERM: the Editor never +Do not burn time re-investigating the Editor side when the error is EPERM: the Editor never saw the connection attempt.