Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions .claude/skills/add-rtk/REMOVE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ Idempotent — safe to run even if some steps were never applied. Run Steps 1–

## 1. Remove the mount from the container config

Read the current mounts, drop the entry whose `containerPath` is `/usr/local/bin/rtk`, and write the rest back.
Read the current mounts, drop the rtk entry, and write the rest back. Match **both** the current `containerPath` (`bin/rtk`) and the legacy absolute one (`/usr/local/bin/rtk`) so installs from any version of the skill are cleaned up.

```bash
pnpm exec tsx scripts/q.ts data/v2.db \
"SELECT additional_mounts FROM container_configs WHERE agent_group_id = '<group-id>'"
```

Write the filtered array (omit any entry with `"containerPath":"/usr/local/bin/rtk"`):
Write the filtered array (omit any entry whose `containerPath` is `bin/rtk` **or** `/usr/local/bin/rtk`):

```bash
pnpm exec tsx scripts/q.ts data/v2.db \
Expand All @@ -20,27 +20,39 @@ pnpm exec tsx scripts/q.ts data/v2.db \

If no rtk entry is present, leave the array as-is.

## 2. Remove the PreToolUse hook from settings.json
## 2. Remove PATH + the PreToolUse hook from settings.json

Delete the rtk Bash hook entry (not comment it out). This leaves any other `PreToolUse` entries intact and is safe to re-run:
Delete the rtk Bash hook (matching both the bare and absolute forms) and drop the `env.PATH` key this skill added. This leaves any other `PreToolUse` entries and `env` keys intact and is safe to re-run:

```bash
SETTINGS="data/v2-sessions/<group-id>/.claude-shared/settings.json"

jq '.hooks.PreToolUse = ((.hooks.PreToolUse // [])
| map(select((.hooks // []) | any(.command == "rtk hook claude") | not)))' \
jq '
del(.env.PATH)
| .hooks.PreToolUse = ((.hooks.PreToolUse // [])
| map(select((.hooks // []) | any(.command | test("rtk hook claude$")) | not)))' \
"$SETTINGS" > /tmp/rtk-settings.json && mv /tmp/rtk-settings.json "$SETTINGS"
```

> The rtk `PATH` lived in a dedicated `env.PATH` key that only this skill set, so deleting it is safe. If you had set `PATH` in `settings.json` for another reason, restore that value instead of deleting the key.

## 3. Restart the container

```bash
ncl groups restart --id <group-id>
```

## 4. Remove the host binary (optional)
## 4. Remove the allowlist entry (optional)

Once no group mounts rtk anymore, drop the rtk entry from `~/.config/nanoclaw/mount-allowlist.json` (`allowedRoots`) and restart the host service so the cache reloads:

```bash
source setup/lib/install-slug.sh
# Linux: systemctl --user restart "$(systemd_unit)"
# macOS: launchctl kickstart -k gui/$(id -u)/"$(launchd_label)"
```

Once no group mounts rtk anymore, remove the binary:
## 5. Remove the host binary (optional)

```bash
rm -f ~/.local/bin/rtk
Expand Down
100 changes: 75 additions & 25 deletions .claude/skills/add-rtk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ Install [rtk](https://github.com/rtk-ai/rtk) — a CLI proxy delivering 60–90%
## What this sets up

- `rtk` binary at `~/.local/bin/rtk` on the host
- `~/.local/bin/rtk` mounted read-only at `/usr/local/bin/rtk` inside the target agent group's containers
- `PreToolUse` hook in the agent group's `settings.json` so every Bash call is automatically filtered through rtk — no CLAUDE.md instructions needed
- That host path allowed for mounting in `~/.config/nanoclaw/mount-allowlist.json`
- `~/.local/bin/rtk` mounted read-only at `/workspace/extra/bin/rtk` inside the target agent group's containers (mount-security forces every additional mount under `/workspace/extra/`, so the `containerPath` **must be relative** — see the mount-security note below)
- `/workspace/extra/bin` added to the container's `PATH` via the agent group's `settings.json` `env`, so the bare `rtk` command that the hook rewrites resolves
- `PreToolUse` hook in the agent group's `settings.json` (invoked by absolute path) so every Bash call is automatically filtered through rtk — no CLAUDE.md instructions needed

> **Migrating from an earlier version of this skill?** An older `/add-rtk` mounted rtk at an **absolute** `containerPath` (`/usr/local/bin/rtk`). On NanoClaw v2 that mount is silently **rejected** by mount-security (see the note below), so rtk never actually reached the container and the hook failed quietly. Symptoms: rtk appears wired but has no effect; `logs/nanoclaw.error.log` shows `Additional mount REJECTED`; `container_configs.additional_mounts` still contains `"containerPath":"/usr/local/bin/rtk"`. **Fix = just re-run this skill** — every step below detects and replaces the old broken entry (mount, allowlist, PATH, hook), then restarts. It is safe to re-run.

> **mount-security note.** `src/modules/mount-security/index.ts` requires every additional mount's `containerPath` to be **relative** and force-prefixes it under `/workspace/extra/`. Absolute container paths (like `/usr/local/bin/rtk`) are rejected outright — that is a deliberate guard against overwriting container system files, not a bug to work around. rtk therefore lands at `/workspace/extra/bin/rtk`, and we put that directory on `PATH` instead.

## Step 1 — Install rtk on the host

Expand Down Expand Up @@ -39,9 +45,27 @@ chmod +x ~/.local/bin/rtk # if needed
ncl groups list
```

Note the group ID (e.g. `ag-1776342942165-ptgddd`). Repeat Steps 3–5 for each group.
Note the group ID (e.g. `ag-1776342942165-ptgddd`). Repeat Steps 4–6 for each group.

## Step 3 — Allow the rtk path in the mount allowlist

Additional mounts are gated by `~/.config/nanoclaw/mount-allowlist.json` — a host path is only mountable if it falls under an entry in `allowedRoots`. Add rtk as a single-file, read-only root (no `allowReadWrite` → forced read-only):

```json
{ "path": "/home/<user>/.local/bin/rtk", "description": "rtk token-optimizer binary (ro)" }
```

Append that object to the `allowedRoots` array (you can also use `/manage-mounts`). Granting the exact file — not the whole `~/.local/bin` — keeps the surface minimal.

The allowlist is cached in-process, so **restart the host service** after editing:

```bash
source setup/lib/install-slug.sh
# Linux: systemctl --user restart "$(systemd_unit)"
# macOS: launchctl kickstart -k gui/$(id -u)/"$(launchd_label)"
```

## Step 3 — Mount rtk into the container config
## Step 4 — Mount rtk into the container config

`additional_mounts` is a JSON array column on `container_configs`. Read the current value, merge in the rtk entry, and write the merged array back.

Expand All @@ -52,13 +76,13 @@ pnpm exec tsx scripts/q.ts data/v2.db \
"SELECT additional_mounts FROM container_configs WHERE agent_group_id = '<group-id>'"
```

Build the merged array: keep every existing entry, drop any entry whose `containerPath` is `/usr/local/bin/rtk` (so re-running replaces rather than duplicates), then add the rtk entry:
Build the merged array: keep every existing entry, **drop any entry whose `containerPath` is `bin/rtk` OR the legacy `/usr/local/bin/rtk`** (so re-running replaces rather than duplicates, and heals installs from the older skill), then add the rtk entry:

```json
{"hostPath":"/home/<user>/.local/bin/rtk","containerPath":"/usr/local/bin/rtk","readonly":true}
{"hostPath":"/home/<user>/.local/bin/rtk","containerPath":"bin/rtk","readonly":true}
```

Write the merged array back:
The relative `bin/rtk` is force-prefixed to `/workspace/extra/bin/rtk` at spawn time. Write the merged array back:

```bash
pnpm exec tsx scripts/q.ts data/v2.db \
Expand All @@ -72,7 +96,7 @@ pnpm exec tsx scripts/q.ts data/v2.db \
"SELECT additional_mounts FROM container_configs WHERE agent_group_id = '<group-id>'"
```

## Step 4 — Add the PreToolUse hook to settings.json
## Step 5 — Add PATH + the PreToolUse hook to settings.json

Each agent group has a `settings.json` at:

Expand All @@ -82,50 +106,76 @@ data/v2-sessions/<group-id>/.claude-shared/settings.json

This file is mounted at `/home/node/.claude/settings.json` inside the container and is read by Claude Code for hooks, env, and model config.

Add the `PreToolUse` entry with `jq`. This drops any existing rtk Bash hook first, then appends a fresh one, so it is safe to re-run without creating duplicates:
Two edits, both via one `jq` pass (safe to re-run — dedups the hook and overwrites the PATH key):

1. **PATH** — the rewritten command rtk emits is the bare name `rtk …`, so `rtk` must be on `PATH`. `/workspace/extra/bin` is not on the image `PATH` by default, and Claude Code's `settings.json` `env` values are literal (no `${PATH}` expansion), so set the full container `PATH` plus `/workspace/extra/bin`. The value below matches the stock agent image (`node:22-slim` base + `/pnpm`); if you run a **non-default provider that extends PATH**, append its entries here too.
2. **Hook** — invoke rtk by **absolute path** so the hook itself never depends on `PATH`; only the rewritten command relies on the `env.PATH` above (the best-supported case, since it runs as a Bash-tool subprocess).

```bash
SETTINGS="data/v2-sessions/<group-id>/.claude-shared/settings.json"
PATHVAL="/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/workspace/extra/bin"

jq '.hooks.PreToolUse = ((.hooks.PreToolUse // [])
| map(select((.hooks // []) | any(.command == "rtk hook claude") | not)))
+ [{"matcher":"Bash","hooks":[{"type":"command","command":"rtk hook claude"}]}]' \
jq --arg p "$PATHVAL" '
.env.PATH = $p
| .hooks.PreToolUse = ((.hooks.PreToolUse // [])
| map(select((.hooks // []) | any(.command | test("rtk hook claude$")) | not)))
+ [{"matcher":"Bash","hooks":[{"type":"command","command":"/workspace/extra/bin/rtk hook claude"}]}]' \
"$SETTINGS" > /tmp/rtk-settings.json && mv /tmp/rtk-settings.json "$SETTINGS"
```

## Step 5 — Restart the container
The `test("rtk hook claude$")` selector matches both the legacy bare `rtk hook claude` and the new absolute form, so re-running (or migrating from the old skill) never leaves a duplicate.

## Step 6 — Restart the container

```bash
ncl groups restart --id <group-id>
```

## Verify

Confirm the binary is executable inside the container so a missing or non-executable mount surfaces immediately rather than as a silent hook failure:
Confirm the binary is mounted, read-only, and executable inside the container so a missing or rejected mount surfaces immediately rather than as a silent hook failure:

```bash
docker exec "$(docker ps --filter "name=<group-id>" --format '{{.Names}}' | head -1)" rtk --version
c="$(docker ps --filter "name=<group-id>" --format '{{.Names}}' | head -1)"
docker exec "$c" sh -c 'ls -l /workspace/extra/bin/rtk && /workspace/extra/bin/rtk --version'
docker inspect "$c" --format '{{range .Mounts}}{{if eq .Destination "/workspace/extra/bin/rtk"}}RW={{.RW}}{{end}}{{end}}' # expect RW=false
```

Then ask the agent to run `git status` or any other supported command. rtk intercepts it silently. Check savings with:
Then ask the agent to run `git status` or any other supported command. rtk intercepts it silently. Each container keeps its **own** rtk stats (separate `HOME`, reset on respawn), so check savings **inside the container**, not on the host:

```bash
~/.local/bin/rtk gain
docker exec "$c" rtk gain
```

## Troubleshooting

### `rtk: command not found` inside the container
## Updating rtk later

Mount wasn't applied or container wasn't restarted:
A running container binds the rtk file by **inode**. The usual update path (download + `mv` into place) creates a **new inode**, so a container that is already running keeps the old version — it does **not** update live. New/respawned containers pick up the new binary automatically. To push a new rtk into a live container, restart it:

```bash
pnpm exec tsx scripts/q.ts data/v2.db \
"SELECT additional_mounts FROM container_configs WHERE agent_group_id = '<group-id>'"
# Look for entry with /usr/local/bin/rtk
ncl groups restart --id <group-id>
```

## Troubleshooting

### `rtk: command not found` inside the container

Work down the two independent gates:

1. **Was the mount rejected?** Check the host log first — this is the real failure signal:
```bash
grep 'Additional mount REJECTED' logs/nanoclaw.error.log | tail
```
Two common reasons:
- `must be relative` → the `containerPath` is absolute (e.g. the legacy `/usr/local/bin/rtk`). Re-run **Step 4** with the relative `bin/rtk`.
- not under an allowed root → the rtk path is missing from the allowlist. Do **Step 3** (and restart the host service).
2. **Is the mount present but not on PATH?** Confirm the file is there and `env.PATH` includes `/workspace/extra/bin`:
```bash
pnpm exec tsx scripts/q.ts data/v2.db \
"SELECT additional_mounts FROM container_configs WHERE agent_group_id = '<group-id>'" # expect "containerPath":"bin/rtk"
jq '.env.PATH' data/v2-sessions/<group-id>/.claude-shared/settings.json # expect …:/workspace/extra/bin
```
Then `ncl groups restart --id <group-id>`.

### Hook not firing

Verify the hook is in `settings.json`:
Expand All @@ -134,7 +184,7 @@ Verify the hook is in `settings.json`:
jq '.hooks.PreToolUse' data/v2-sessions/<group-id>/.claude-shared/settings.json
```

If missing, re-run Step 4.
If missing, re-run Step 5.

### Binary won't execute — permission denied

Expand Down
Loading