diff --git a/AGENTS.md b/AGENTS.md index 45301ec4a..7e3685934 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 000000000..7caada753 --- /dev/null +++ b/docs/claude-code-sandbox.md @@ -0,0 +1,76 @@ +# 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 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 + 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-/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. 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 + +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 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 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 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 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 + +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 *"` (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 when the error is EPERM: the Editor never +saw the connection attempt.