diff --git a/bin/clipboard-copy b/bin/clipboard-copy index 2121522..7a593aa 100755 --- a/bin/clipboard-copy +++ b/bin/clipboard-copy @@ -18,5 +18,15 @@ elif [ -n "$DISPLAY" ] && command -v xclip >/dev/null 2>&1; then setsid -f xclip -selection clipboard else data=$(base64 | tr -d '\n') - printf '\033]52;c;%s\a' "$data" > /dev/tty + # /dev/tty needs this process's own session to have a controlling + # terminal, but a job spawned by nvim's clipboard provider doesn't get + # one (nvim's job-spawning doesn't establish a ctty for children). + # Resolve the real terminal from the parent's (nvim's) recorded tty + # instead, which doesn't depend on this process having its own ctty. + real_tty="/dev/$(ps -o tty= -p "$PPID" 2>/dev/null | tr -d ' ')" + if [ -w "$real_tty" ]; then + printf '\033]52;c;%s\a' "$data" > "$real_tty" + else + printf '\033]52;c;%s\a' "$data" > /dev/tty + fi fi diff --git a/docker/claude-sandbox/README.md b/docker/claude-sandbox/README.md index 45d836d..189ee32 100644 --- a/docker/claude-sandbox/README.md +++ b/docker/claude-sandbox/README.md @@ -82,8 +82,10 @@ On start, the container runs an allowlist-only `iptables`/`ipset` firewall (`init-firewall.sh`), matching the reference dev container. Allowed by default: DNS resolution via Docker's embedded resolver (loopback only, so never leaves the container), the Docker host itself (not its whole subnet), -GitHub's published IP ranges, `registry.npmjs.org`, `pypi.org` and -`files.pythonhosted.org` (mason's Python-based LSP tools - `ruff`, +GitHub's published IP ranges, `release-assets.githubusercontent.com` +(GitHub release assets - e.g. mason's own registry index - redirect here, +a separate CDN from github.com's own IPs), `registry.npmjs.org`, `pypi.org` +and `files.pythonhosted.org` (mason's Python-based LSP tools - `ruff`, `basedpyright`, `debugpy` - install via pip), `api.anthropic.com`, and whatever hosts `GITEA_SSH_HOST`/`GITEA_API` resolve to — that last group isn't port-restricted, so git-over-ssh to GitHub/Gitea works without a diff --git a/docker/claude-sandbox/entrypoint.sh b/docker/claude-sandbox/entrypoint.sh index 1b9c466..532e6ee 100755 --- a/docker/claude-sandbox/entrypoint.sh +++ b/docker/claude-sandbox/entrypoint.sh @@ -14,6 +14,15 @@ if [ "${ENABLE_FIREWALL:-1}" = "1" ]; then EXTRA_ALLOWED_DOMAINS="${extra_domains}" /usr/local/bin/init-firewall.sh fi +# A normal login/ssh session always chowns the allocated pty to the logging +# in user; this container's entrypoint skips that (there's no login/sshd +# involved), leaving the tty root-owned. nvim's clipboard fallback needs to +# write to it directly as node, so fix the ownership up here, before +# dropping privileges. +if [ -t 1 ]; then + chown node "$(tty)" 2>/dev/null || true +fi + # Drop root -> node for the actual session; node has no sudo/setuid path # back to root after this, so it can't re-run init-firewall.sh itself to # widen the allowlist mid-session. diff --git a/docker/claude-sandbox/init-firewall.sh b/docker/claude-sandbox/init-firewall.sh index 484de62..f7f67e9 100755 --- a/docker/claude-sandbox/init-firewall.sh +++ b/docker/claude-sandbox/init-firewall.sh @@ -65,7 +65,8 @@ for domain in \ "api.anthropic.com" \ "gitlab.com" \ "pypi.org" \ - "files.pythonhosted.org"; do + "files.pythonhosted.org" \ + "release-assets.githubusercontent.com"; do echo "Resolving $domain..." ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}') if [ -z "$ips" ]; then