From a36c1f62db337513485be6e8ad5a68b2046d87c3 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sun, 19 Jul 2026 18:19:50 +0200 Subject: [PATCH] fix(docker): set CLAUDE_CODE_TMPDIR + document hook reachability limit Found in live testing: claude refuses its default /tmp/claude- temp dir when that path pre-exists root-owned (happens when the workspace bind-mount traverses it, e.g. a workspace under /tmp/claude-). Set CLAUDE_CODE_TMPDIR to a nonexistent HOME subpath the running uid creates+owns, so docker claude sessions are robust to any workspace location. Also document the hook-reachability constraint: in-container hooks POST to host.docker.internal (the bridge gateway), so they only fire when Codeman is reachable from the container (bind 0.0.0.0 + password); on a loopback-only bind they don't fire and idle detection falls back to output-based (which works). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/docker-cases.md | 9 +++++++++ src/tmux-manager.ts | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/docs/docker-cases.md b/docs/docker-cases.md index 21b4f3d8..d929de8b 100644 --- a/docs/docker-cases.md +++ b/docs/docker-cases.md @@ -60,6 +60,15 @@ The container is paused across the capture so the image and workspace are consis `GET /api/docker-exports` lists bundles; `GET /api/docker-exports/:filename` downloads one; `DELETE` removes one. +## Hooks require the server to be reachable from the container + +In-container hooks (permission events, hook-based idle/stop/task notifications) POST to `CODEMAN_API_URL`, which is derived as `https://host.docker.internal:` (`host.docker.internal` → the docker bridge gateway, e.g. `172.17.0.1`, via `--add-host …:host-gateway`). For that callback to succeed, the Codeman server must be **listening on an interface the container can reach**. + +- If Codeman binds **loopback-only** (`127.0.0.1`, the default and the production systemd config), a container reaching `172.17.0.1:` cannot connect, so **in-container hooks do not fire**. The session still works fully: idle/stop detection falls back to **output-based** detection through the `docker exec` PTY (which always works), and claude runs with `--dangerously-skip-permissions` so there are no permission prompts to forward anyway. +- To enable in-container hooks, run Codeman where the container can reach it: bind `0.0.0.0` **with `CODEMAN_PASSWORD` set** (`CODEMAN_HOST=0.0.0.0`), or otherwise make `172.17.0.1:` reachable. The host guard already allowlists `host.docker.internal` / `host.containers.internal`, and the hook secret is mounted, so hooks work as soon as the callback is reachable. + +This is an environmental constraint, not a code limitation: the host-gateway mapping, `CODEMAN_API_URL` derivation, host-guard allowlist, and hook-secret mount are all wired correctly. + ## Notes & limits - Requires Docker (or Podman) with a reachable daemon; tmux must be present in the base image (a hard prerequisite, probed at link time). diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index 9c29b952..b1914d50 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -997,6 +997,12 @@ export function resolveDockerLaunchOptions( HOME: CONTAINER_HOME, TERM: 'xterm-256color', COLORTERM: 'truecolor', + // Give claude a temp dir it will own inside HOME. Its default `/tmp/claude-` + // is refused when that path pre-exists root-owned — which happens when the + // workspace bind-mount path traverses it (e.g. a workspace under /tmp/claude-). + // A nonexistent HOME subpath is created+owned by the running uid, so this is robust + // to any workspace location. Non-secret path, safe to be committed on export. + CLAUDE_CODE_TMPDIR: `${CONTAINER_HOME}/.cache/codeman-claude-tmp`, }; if (docker.hooksEnabled) { // Derive a container-reachable API url (scheme + port preserved; host swapped