Skip to content

Commit 2530991

Browse files
authored
run: quiet relay diagnostics, fix log prefix, document no-root (#659)
1 parent f70929d commit 2530991

3 files changed

Lines changed: 75 additions & 24 deletions

File tree

cmd/clawpatrol/integrations.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ func installClaudeCodeOAuthShim(cmd []string) {
400400
dir = filepath.Join(defaultClawpatrolDir(), "claude-config")
401401
}
402402
if err := os.MkdirAll(dir, 0o700); err != nil {
403-
log.Printf("clawpatrol: claude oauth shim: mkdir %s: %v", dir, err)
403+
fmt.Fprintf(os.Stderr, "[clawpatrol] claude oauth shim: mkdir %s: %v\n", dir, err)
404404
return
405405
}
406406
credPath := filepath.Join(dir, ".credentials.json")
@@ -414,7 +414,7 @@ func installClaudeCodeOAuthShim(cmd []string) {
414414
}
415415
}
416416
if err := writeClaudeCodeCredentials(credPath, bearer); err != nil {
417-
log.Printf("clawpatrol: claude oauth shim: write credentials: %v", err)
417+
fmt.Fprintf(os.Stderr, "[clawpatrol] claude oauth shim: write credentials: %v\n", err)
418418
return
419419
}
420420
// Redirect Claude Code's credential read onto our synthesized file
@@ -433,7 +433,7 @@ func installClaudeCodeOAuthShim(cmd []string) {
433433
// makes them work. Printed instead of silently rewriting the worker's
434434
// environment and config dir (R&D decision, 2026-06-03).
435435
func warnClaudeCodeRemoteControlDisabled() {
436-
log.Printf(`clawpatrol: Claude Code /remote-control and other claude.ai
436+
fmt.Fprintf(os.Stderr, `[clawpatrol] Claude Code /remote-control and other claude.ai
437437
subscription-only features are disabled in this session: ANTHROPIC_AUTH_TOKEN
438438
is set, so Claude Code treats this as API-key auth and gates them off locally.
439439
@@ -444,7 +444,8 @@ To enable them, opt into the OAuth shim:
444444
The shim writes a synthesized .credentials.json and points CLAUDE_CONFIG_DIR at
445445
it for the child. Because that shadows your existing ~/.claude (skills, memory,
446446
MCP servers, project state), it is off by default — set CLAUDE_CONFIG_DIR to
447-
your own dir first if you want both. See doc/claude-code-oauth.md.`)
447+
your own dir first if you want both. See doc/claude-code-oauth.md.
448+
`)
448449
}
449450

450451
// writeClaudeCodeCredentials emits the JSON shape Claude Code's

cmd/clawpatrol/relay_linux.go

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,25 @@ import (
4646
"golang.org/x/sys/unix"
4747
)
4848

49+
// relayDebug gates the relay / relay-worker diagnostic chatter (port
50+
// auto-expose, the host-loopback forwarder line, per-connection
51+
// errors). It's noise during a normal `clawpatrol run`, so it's off
52+
// unless CLAWPATROL_DEBUG is set. Genuine functional warnings (the ⚠
53+
// lines) print regardless. Evaluated once — the env is inherited
54+
// across the relay-worker re-exec.
55+
var relayDebug = func() bool {
56+
v := os.Getenv("CLAWPATROL_DEBUG")
57+
return v != "" && v != "0"
58+
}()
59+
60+
// relayDebugf writes a relay diagnostic line to stderr only when
61+
// CLAWPATROL_DEBUG is set.
62+
func relayDebugf(format string, a ...any) {
63+
if relayDebug {
64+
fmt.Fprintf(os.Stderr, format, a...)
65+
}
66+
}
67+
4968
// --- BPF + seccomp ABI (re-declared because x/sys/unix has no SockFprog
5069
// helper for the SECCOMP_SET_MODE_FILTER syscall) ---------------------
5170

@@ -234,7 +253,7 @@ func runRelaySupervisor(_ []string) {
234253
// and we don't want it auto-exposed back to the host.
235254
workerPID, err := recvWorkerPID(lbRC)
236255
if err != nil {
237-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] read worker pid: %v\n", err)
256+
relayDebugf("[clawpatrol relay] read worker pid: %v\n", err)
238257
return
239258
}
240259

@@ -257,7 +276,7 @@ func runRelaySupervisor(_ []string) {
257276
if errors.Is(err, unix.ENOENT) {
258277
return
259278
}
260-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] notif_recv: %v\n", err)
279+
relayDebugf("[clawpatrol relay] notif_recv: %v\n", err)
261280
return
262281
}
263282

@@ -275,7 +294,7 @@ func runRelaySupervisor(_ []string) {
275294
_ = notifSendContinue(notifyFD, n.ID)
276295

277296
if perr != nil {
278-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] inspect listen sockfd: %v\n", perr)
297+
relayDebugf("[clawpatrol relay] inspect listen sockfd: %v\n", perr)
279298
continue
280299
}
281300
seenMu.Lock()
@@ -290,13 +309,13 @@ func runRelaySupervisor(_ []string) {
290309
host := mirrorBindScope(family, ip)
291310
ln, lerr := net.Listen("tcp", net.JoinHostPort(host, fmt.Sprintf("%d", port)))
292311
if lerr != nil {
293-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] could not tunnel %s:%d: %v\n", host, port, lerr)
312+
relayDebugf("[clawpatrol relay] could not tunnel %s:%d: %v\n", host, port, lerr)
294313
seenMu.Lock()
295314
delete(seen, port)
296315
seenMu.Unlock()
297316
continue
298317
}
299-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] auto-expose %s:%d → agent netns\n", host, port)
318+
relayDebugf("[clawpatrol relay] auto-expose %s:%d → agent netns\n", host, port)
300319
go acceptLoop(ln, port, workerRC)
301320
} else {
302321
_ = notifSendContinue(notifyFD, n.ID)
@@ -349,7 +368,7 @@ func runLoopbackSupervisorLoop(lbRC syscall.RawConn) {
349368
if errors.Is(err, io.EOF) {
350369
return
351370
}
352-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] loopback recv: %v\n", err)
371+
relayDebugf("[clawpatrol relay] loopback recv: %v\n", err)
353372
return
354373
}
355374
go handleLoopbackJob(ip, port, fd)
@@ -369,12 +388,12 @@ func handleLoopbackJob(ip [4]byte, port uint16, fd int) {
369388

370389
dst := net.IPv4(ip[0], ip[1], ip[2], ip[3])
371390
if !dst.IsLoopback() {
372-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] refusing non-loopback dst %s\n", dst)
391+
relayDebugf("[clawpatrol relay] refusing non-loopback dst %s\n", dst)
373392
return
374393
}
375394
host, err := net.Dial("tcp", net.JoinHostPort(dst.String(), fmt.Sprintf("%d", port)))
376395
if err != nil {
377-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] dial host %s:%d: %v\n", dst, port, err)
396+
relayDebugf("[clawpatrol relay] dial host %s:%d: %v\n", dst, port, err)
378397
return
379398
}
380399
defer func() { _ = host.Close() }()
@@ -501,7 +520,7 @@ func procPeekListener(pid, sockfd int) (uint16, net.IP, int, error) {
501520
port, ip, ok, err := scanProcNetTCP(t.path, inode, t.ipHex)
502521
if err != nil && !errors.Is(err, os.ErrNotExist) {
503522
// Surface IO errors but keep trying the other family.
504-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] read %s: %v\n", t.path, err)
523+
relayDebugf("[clawpatrol relay] read %s: %v\n", t.path, err)
505524
continue
506525
}
507526
if ok {
@@ -623,12 +642,12 @@ func acceptLoop(ln net.Listener, port uint16, workerRC syscall.RawConn) {
623642
for {
624643
c, err := ln.Accept()
625644
if err != nil {
626-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] accept on :%d ended: %v\n", port, err)
645+
relayDebugf("[clawpatrol relay] accept on :%d ended: %v\n", port, err)
627646
return
628647
}
629648
fd, perr := tcpRawFD(c)
630649
if perr != nil {
631-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] raw fd on :%d: %v\n", port, perr)
650+
relayDebugf("[clawpatrol relay] raw fd on :%d: %v\n", port, perr)
632651
_ = c.Close()
633652
continue
634653
}
@@ -638,7 +657,7 @@ func acceptLoop(ln net.Listener, port uint16, workerRC syscall.RawConn) {
638657
err = sendJob(workerRC, portBuf[:], rights)
639658
_ = c.Close()
640659
if err != nil {
641-
fmt.Fprintf(os.Stderr, "[clawpatrol relay] sendmsg to worker on :%d: %v\n", port, err)
660+
relayDebugf("[clawpatrol relay] sendmsg to worker on :%d: %v\n", port, err)
642661
return
643662
}
644663
}
@@ -723,7 +742,7 @@ func runRelayWorker(_ []string) {
723742
// Tell the supervisor our PID so it can ignore the listen() trap
724743
// triggered by our own host-loopback forwarder below.
725744
if err := sendWorkerPID(lbRC); err != nil {
726-
fmt.Fprintf(os.Stderr, "[clawpatrol relay-worker] send pid: %v\n", err)
745+
relayDebugf("[clawpatrol relay-worker] send pid: %v\n", err)
727746
// Continue — supervisor's loopback loop will block on recv
728747
// and the auto-expose direction will still work.
729748
}
@@ -769,7 +788,7 @@ func relayWorkerLoop(rc syscall.RawConn, handle func(uint16, int)) {
769788
if errors.Is(err, io.EOF) {
770789
return
771790
}
772-
fmt.Fprintf(os.Stderr, "[clawpatrol relay-worker] recv: %v\n", err)
791+
relayDebugf("[clawpatrol relay-worker] recv: %v\n", err)
773792
return
774793
}
775794
go handle(port, fd)
@@ -901,7 +920,7 @@ func handleJob(port uint16, fd int) {
901920

902921
inner, err := dialAgentLoopback(port)
903922
if err != nil {
904-
fmt.Fprintf(os.Stderr, "[clawpatrol relay-worker] dial 127.0.0.1:%d: %v\n", port, err)
923+
relayDebugf("[clawpatrol relay-worker] dial 127.0.0.1:%d: %v\n", port, err)
905924
return
906925
}
907926
defer func() { _ = inner.Close() }()
@@ -981,7 +1000,7 @@ func setupHostLoopbackForwarder(lbRC syscall.RawConn) error {
9811000
_ = ln.Close()
9821001
return fmt.Errorf("install REDIRECT rules: %w", err)
9831002
}
984-
fmt.Fprintf(os.Stderr, "[clawpatrol relay-worker] host-loopback forwarder on 127.0.0.1:%d (REDIRECT installed)\n", fwdPort)
1003+
relayDebugf("[clawpatrol relay-worker] host-loopback forwarder on 127.0.0.1:%d (REDIRECT installed)\n", fwdPort)
9851004
go loopbackAcceptLoop(ln, lbRC)
9861005
return nil
9871006
}
@@ -1065,18 +1084,18 @@ func loopbackAcceptLoop(ln net.Listener, lbRC syscall.RawConn) {
10651084
for {
10661085
c, err := ln.Accept()
10671086
if err != nil {
1068-
fmt.Fprintf(os.Stderr, "[clawpatrol relay-worker] loopback accept ended: %v\n", err)
1087+
relayDebugf("[clawpatrol relay-worker] loopback accept ended: %v\n", err)
10691088
return
10701089
}
10711090
fd, perr := tcpRawFD(c)
10721091
if perr != nil {
1073-
fmt.Fprintf(os.Stderr, "[clawpatrol relay-worker] loopback raw fd: %v\n", perr)
1092+
relayDebugf("[clawpatrol relay-worker] loopback raw fd: %v\n", perr)
10741093
_ = c.Close()
10751094
continue
10761095
}
10771096
origIP, origPort, perr := getOriginalDst(fd)
10781097
if perr != nil {
1079-
fmt.Fprintf(os.Stderr, "[clawpatrol relay-worker] SO_ORIGINAL_DST: %v\n", perr)
1098+
relayDebugf("[clawpatrol relay-worker] SO_ORIGINAL_DST: %v\n", perr)
10801099
_ = c.Close()
10811100
continue
10821101
}
@@ -1085,7 +1104,7 @@ func loopbackAcceptLoop(ln net.Listener, lbRC syscall.RawConn) {
10851104
err = sendJob(lbRC, frame[:], rights)
10861105
_ = c.Close()
10871106
if err != nil {
1088-
fmt.Fprintf(os.Stderr, "[clawpatrol relay-worker] loopback sendmsg: %v\n", err)
1107+
relayDebugf("[clawpatrol relay-worker] loopback sendmsg: %v\n", err)
10891108
return
10901109
}
10911110
}

site/doc/cli.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,36 @@ The agent sees a normal network — outbound flows just route through
9595
the gateway, which matches each request against the rules, injects
9696
the configured credential, and forwards.
9797

98+
#### No root inside `clawpatrol run` (Linux)
99+
100+
The wrapped command runs in an unprivileged user namespace. As a
101+
consequence of how Linux user namespaces work, that namespace has no
102+
mapped root: your own uid is the only one mapped in, host root (uid 0)
103+
is not. So commands that need real root won't work — `sudo` fails
104+
with messages like:
105+
106+
```
107+
sudo: /etc/sudo.conf is owned by uid 65534, should be 0
108+
sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
109+
```
110+
111+
The first is the namespace mapping root-owned files to "nobody"
112+
(65534); the second is the `no_new_privileges` flag clawpatrol sets to
113+
install its unprivileged seccomp filter. This isn't a deliberate
114+
restriction — it falls out of running unprivileged, and the host's own
115+
`sudo` is unaffected outside the wrapper.
116+
117+
If a command needs to act as root:
118+
119+
- **Install the tooling on the host first**, then launch — e.g.
120+
`sudo apt-get install -y postgresql-client && clawpatrol run -- psql …`.
121+
Many tools can also be unpacked into a local prefix (e.g.
122+
`dpkg-deb -x`) without root at all.
123+
- **Use `--whole-machine`** (see `clawpatrol join --whole-machine`).
124+
It routes traffic at the host level instead of per-process, so the
125+
command runs in the normal host environment where `sudo` works —
126+
you run it directly, not through `clawpatrol run`.
127+
98128
### `clawpatrol test`
99129

100130
Replay recorded gateway actions against a candidate HCL policy and
@@ -174,6 +204,7 @@ device-side knobs:
174204
| Variable | Effect |
175205
|---|---|
176206
| `CLAWPATROL_RUN_CONF` | Override the WG conf path `clawpatrol run` reads |
207+
| `CLAWPATROL_DEBUG` | Print the relay / auto-expose diagnostic lines, which are otherwise silent |
177208
| `CLAWPATROL_NO_ENV` | Skip the env pushdown (`SSL_CERT_FILE`, placeholders) when wrapping a command |
178209
| `CLAWPATROL_TELEMETRY` | `0` to disable telemetry (same as `DO_NOT_TRACK=1`) |
179210
| `DO_NOT_TRACK` | Standard opt-out, honored |

0 commit comments

Comments
 (0)