369 lines
18 KiB
Markdown
369 lines
18 KiB
Markdown
---
|
|
phase: 260505-le7
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- claudebox.sh
|
|
autonomous: true
|
|
requirements: [HARNESS-01, HARNESS-02, HARNESS-03, HARNESS-04]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "User can put `cmd = gsd` in `~/.claudebox/config` or `<project>/.claudebox` and claudebox launches gsd inside the sandbox instead of claude"
|
|
- "User can put `mount_home = .gsd` in a config file and `~/.gsd` is rw-bound into the sandbox"
|
|
- "User can put `path_add = ~/.local/share/npm/bin` in a config file and that dir appears prepended to PATH inside the sandbox"
|
|
- "Per-project `.claudebox` overrides global `~/.claudebox/config` for `cmd` (last-wins), but `mount_home` and `path_add` accumulate across both files"
|
|
- "CLI flags `--cmd`, `--mount-home`, `--path-add` override/append on top of config files with same semantics"
|
|
- "When `cmd` resolves to anything other than the default `claude` binary, `--dangerously-skip-permissions` is NOT prepended"
|
|
- "Audit shows a `[config]` section listing which config files were loaded; extra mounts appear in Mounts section; path additions appear in PATH list"
|
|
- "`--dry-run` reflects extra mounts and PATH additions"
|
|
- "`--check` reports presence/absence of `~/.claudebox/config` and `<CWD>/.claudebox`"
|
|
- "If `cmd` resolves to a non-existent binary, claudebox prints an error and exits 1"
|
|
- "If a `mount_home` subdir does not exist on host, claudebox warns but does not error"
|
|
artifacts:
|
|
- path: "claudebox.sh"
|
|
provides: "Config file parsing, CLI flag handling, harness binary resolution, extra mounts, PATH augmentation"
|
|
contains: "load_config_file"
|
|
key_links:
|
|
- from: "config file parser"
|
|
to: "MOUNT_HOME / PATH_ADD / HARNESS_CMD globals"
|
|
via: "load_config_file appends to arrays / sets scalar"
|
|
pattern: "load_config_file"
|
|
- from: "HARNESS_CMD"
|
|
to: "SANDBOX_CMD construction"
|
|
via: "command -v resolution; conditional --dangerously-skip-permissions"
|
|
pattern: "SANDBOX_CMD=.*HARNESS_BIN"
|
|
- from: "PATH_ADD entries"
|
|
to: "SANDBOX_PATH passed via --setenv PATH"
|
|
via: "prepend with `:` separator before bwrap exec"
|
|
pattern: "SANDBOX_PATH="
|
|
- from: "MOUNT_HOME entries"
|
|
to: "BWRAP_ARGS / dry-run output"
|
|
via: "--bind \\$HOME/<sub> \\$HOME/<sub>"
|
|
pattern: "--bind.*HOME"
|
|
---
|
|
|
|
<objective>
|
|
Add config file support to `claudebox` so users can pin alternate harnesses (e.g. `gsd`) and accompanying home mounts / PATH dirs without remembering CLI flags.
|
|
|
|
Purpose: Make claudebox usable as a generic sandbox launcher for non-claude CLIs while keeping claude as the zero-config default.
|
|
|
|
Output: Updated `claudebox.sh` with config file loading, CLI flag overrides, harness binary resolution with conditional `--dangerously-skip-permissions`, extra home mounts, PATH augmentation, audit/dry-run/check integration.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/toph/code/tools/claudebox/.claude/get-shit-done/workflows/execute-plan.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@/home/toph/code/tools/claudebox/CLAUDE.md
|
|
@/home/toph/code/tools/claudebox/claudebox.sh
|
|
@/home/toph/code/tools/claudebox/flake.nix
|
|
|
|
<interfaces>
|
|
<!-- Existing claudebox.sh shape the executor needs to integrate with. -->
|
|
|
|
Current flag parser (lines 11-28): while-loop case statement, `--` ends parsing, unknown args fall through to `CLAUDE_ARGS+=("$1")`.
|
|
|
|
Existing globals set during init:
|
|
- `CWD` (line 178)
|
|
- `CANONICAL_ROOT` (line 199) — also used to locate per-project `.claudebox.env`; reuse for `<project-root>/.claudebox`
|
|
- `HOME`, `SANDBOX_PATH` (injected by flake.nix), `SANDBOX_BASH`, `CLAUDE_BIN`
|
|
|
|
Existing parallel pattern to mimic — env files (lines 386-408):
|
|
```bash
|
|
load_env_file() {
|
|
local file="$1"
|
|
[[ -f "$file" ]] || return 0
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
line="${line#"${line%%[! ]*}"}"
|
|
[[ -z "$line" || "$line" == '#'* ]] && continue
|
|
[[ "$line" != *=* ]] && continue
|
|
local key="${line%%=*}"
|
|
local val="${line#*=}"
|
|
...
|
|
done < "$file"
|
|
}
|
|
load_env_file "$HOME/.claudebox/env"
|
|
load_env_file "$CANONICAL_ROOT/.claudebox.env"
|
|
```
|
|
|
|
SANDBOX_CMD construction (lines 492-496):
|
|
```bash
|
|
if [[ "$SHELL_MODE" == true ]]; then
|
|
SANDBOX_CMD=("$SANDBOX_BASH" "${CLAUDE_ARGS[@]}")
|
|
else
|
|
SANDBOX_CMD=("$CLAUDE_BIN" --dangerously-skip-permissions "${CLAUDE_ARGS[@]}")
|
|
fi
|
|
```
|
|
|
|
BWRAP_ARGS assembly (lines 565-624) and dry-run output (lines 499-563) are parallel — any new mount must be added to BOTH.
|
|
|
|
ENV_ARGS already wires `--setenv PATH "$SANDBOX_PATH"`. To prepend dirs, mutate `SANDBOX_PATH` BEFORE building ENV_ARGS (line 320) OR after, and update `AUDIT_SANDBOX_VALS[PATH]` to match.
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Parse config files and CLI flags; expose HARNESS_CMD / MOUNT_HOME / PATH_ADD globals</name>
|
|
<files>claudebox.sh</files>
|
|
<action>
|
|
Add config file parsing and new CLI flags to `claudebox.sh`.
|
|
|
|
1. **Initialise globals** near the top (alongside `SKIP_AUDIT=false` etc., line 1-9):
|
|
```bash
|
|
HARNESS_CMD="" # set by config or --cmd; empty means "use default claude"
|
|
MOUNT_HOME=() # array of subdir names (relative to $HOME)
|
|
PATH_ADD=() # array of dirs to prepend to sandbox PATH
|
|
CONFIG_FILES_LOADED=() # for audit: list of loaded config paths
|
|
```
|
|
|
|
2. **Add CLI flags** to the while-loop (lines 11-28):
|
|
- `--cmd <binary>` → sets `HARNESS_CMD="$2"; shift`
|
|
- `--mount-home <subdir>` → `MOUNT_HOME+=("$2"); shift`
|
|
- `--path-add <dir>` → `PATH_ADD+=("${2/#\~/$HOME}"); shift`
|
|
- All three must validate the next arg exists and error+exit 1 if missing (mirror the `--ssh-key` pattern at lines 19-22).
|
|
|
|
3. **Add config loader function** alongside `load_env_file` (after line 408 is fine, but it MUST run BEFORE flag parsing applies overrides — see step 4 for ordering). Function:
|
|
```bash
|
|
load_config_file() {
|
|
local file="$1"
|
|
[[ -f "$file" ]] || return 0
|
|
CONFIG_FILES_LOADED+=("$file")
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
line="${line#"${line%%[! ]*}"}" # ltrim
|
|
[[ -z "$line" || "$line" == '#'* ]] && continue
|
|
[[ "$line" != *=* ]] && continue
|
|
local key="${line%%=*}"
|
|
local val="${line#*=}"
|
|
# trim surrounding whitespace from key and val
|
|
key="${key%"${key##*[! ]}"}"; key="${key#"${key%%[! ]*}"}"
|
|
val="${val#"${val%%[! ]*}"}"; val="${val%"${val##*[! ]}"}"
|
|
case "$key" in
|
|
cmd) HARNESS_CMD="$val" ;;
|
|
mount_home) MOUNT_HOME+=("$val") ;;
|
|
path_add) PATH_ADD+=("${val/#\~/$HOME}") ;;
|
|
*) echo "${YELLOW:-}Warning: unknown key '$key' in $file${RESET:-}" >&2 ;;
|
|
esac
|
|
done < "$file"
|
|
}
|
|
```
|
|
|
|
4. **Ordering constraint (CRITICAL)**: Config files must load BEFORE CLI flags take effect, but CANONICAL_ROOT is computed at line 199 — AFTER current flag parsing. Solution: split into two passes.
|
|
- Move `compute_canonical_root` + `CANONICAL_ROOT` computation earlier (right after `CWD=$(pwd)` at line 178), so it is available before config loading. Verify nothing earlier in the script depends on `CWD` having NOT been canonicalised — it doesn't, only `CWD` itself is used.
|
|
- Then load configs in this order (cascading, later overrides earlier scalar / appends to arrays):
|
|
```bash
|
|
load_config_file "$HOME/.claudebox/config"
|
|
load_config_file "$CANONICAL_ROOT/.claudebox"
|
|
```
|
|
- The CLI flag handling already happens at the top of the script. To preserve "CLI overrides config" semantics, capture the CLI values into separate variables during arg parsing (e.g. `CLI_HARNESS_CMD`, `CLI_MOUNT_HOME=()`, `CLI_PATH_ADD=()`), then after config loading apply:
|
|
```bash
|
|
[[ -n "$CLI_HARNESS_CMD" ]] && HARNESS_CMD="$CLI_HARNESS_CMD"
|
|
MOUNT_HOME+=("${CLI_MOUNT_HOME[@]}")
|
|
PATH_ADD+=("${CLI_PATH_ADD[@]}")
|
|
```
|
|
Note the bash gotcha: `MOUNT_HOME+=("${CLI_MOUNT_HOME[@]}")` errors under `set -u` when the array is empty. Use `MOUNT_HOME+=("${CLI_MOUNT_HOME[@]:-}")` guarded by length check, or `(( ${#CLI_MOUNT_HOME[@]} > 0 )) && MOUNT_HOME+=("${CLI_MOUNT_HOME[@]}")`.
|
|
|
|
5. **Resolve harness binary**. After `CLAUDE_BIN="$(command -v claude)"` (line 175), add:
|
|
```bash
|
|
if [[ -n "$HARNESS_CMD" ]]; then
|
|
HARNESS_BIN="$(command -v "$HARNESS_CMD" 2>/dev/null)" || {
|
|
echo "${RED:-}Error: configured cmd '$HARNESS_CMD' not found in PATH${RESET:-}" >&2
|
|
exit 1
|
|
}
|
|
else
|
|
HARNESS_BIN="$CLAUDE_BIN"
|
|
HARNESS_CMD="claude"
|
|
fi
|
|
IS_DEFAULT_CLAUDE=false
|
|
[[ "$HARNESS_BIN" == "$CLAUDE_BIN" ]] && IS_DEFAULT_CLAUDE=true
|
|
```
|
|
Note ANSI colour vars are defined at line 121 — make sure binary resolution happens AFTER that block so error styling works. If config loading must happen before colour-var setup for ordering, fall back to plain text in the error.
|
|
|
|
6. **`--check` additions**: in the CHECK_MODE block (lines 71-112), after the `~/.claudebox` check, add:
|
|
```bash
|
|
if [[ -f "$HOME/.claudebox/config" ]]; then
|
|
echo "${green}OK${reset} ~/.claudebox/config exists" >&2
|
|
else
|
|
echo "${yellow}WARN${reset} ~/.claudebox/config -- not found (optional)" >&2
|
|
fi
|
|
_proj_cfg=$(compute_canonical_root "$PWD")/.claudebox
|
|
if [[ -f "$_proj_cfg" ]]; then
|
|
echo "${green}OK${reset} $_proj_cfg exists" >&2
|
|
else
|
|
echo "${yellow}WARN${reset} $_proj_cfg -- not found (optional)" >&2
|
|
fi
|
|
```
|
|
`compute_canonical_root` is defined at line 181; the function definition must be moved above the CHECK_MODE block too (or duplicated check moved below — simpler: hoist the function definition near the top with the other helpers, alongside `gc_instances`).
|
|
|
|
Do not introduce new external dependencies. Keep everything in pure bash. Preserve `set -euo pipefail` semantics imposed by `writeShellApplication`.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/toph/code/tools/claudebox && nix build .#claudebox 2>&1 | tail -20 && ./result/bin/claudebox --check 2>&1 | grep -E '(claudebox/config|/.claudebox)'</automated>
|
|
</verify>
|
|
<done>
|
|
- New flags `--cmd`, `--mount-home`, `--path-add` parse without error
|
|
- `nix build .#claudebox` succeeds (shellcheck clean)
|
|
- `--check` reports presence/absence of both config file paths
|
|
- HARNESS_BIN resolves correctly; missing harness binary produces error+exit 1
|
|
- `IS_DEFAULT_CLAUDE` flag correctly distinguishes default-claude from override
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Wire HARNESS_BIN, MOUNT_HOME, PATH_ADD into SANDBOX_CMD, BWRAP_ARGS, ENV_ARGS, audit, and dry-run</name>
|
|
<files>claudebox.sh</files>
|
|
<action>
|
|
Consume the globals produced by Task 1 throughout the rest of the pipeline.
|
|
|
|
1. **PATH augmentation**. BEFORE building `ENV_ARGS` (currently at line 320), prepend PATH_ADD entries to SANDBOX_PATH:
|
|
```bash
|
|
if (( ${#PATH_ADD[@]} > 0 )); then
|
|
_path_prefix=""
|
|
for _p in "${PATH_ADD[@]}"; do
|
|
_path_prefix+="${_p}:"
|
|
done
|
|
SANDBOX_PATH="${_path_prefix}${SANDBOX_PATH}"
|
|
unset _path_prefix _p
|
|
fi
|
|
```
|
|
This mutates SANDBOX_PATH before it is consumed by `--setenv PATH "$SANDBOX_PATH"` and by `AUDIT_SANDBOX_VALS[PATH]` — both will reflect the prepended dirs automatically. Verify by inspecting lines 324 and 333 use `$SANDBOX_PATH` directly.
|
|
|
|
2. **MOUNT_HOME validation and bwrap wiring**. After the existing SSH-related mount block in BWRAP_ARGS (after line 616, before the closing `BWRAP_ARGS+=(...)` block at 618):
|
|
```bash
|
|
for _sub in "${MOUNT_HOME[@]}"; do
|
|
_src="$HOME/$_sub"
|
|
if [[ ! -e "$_src" ]]; then
|
|
echo "${YELLOW}Warning: mount_home '$_sub' does not exist at $_src; skipping${RESET}" >&2
|
|
continue
|
|
fi
|
|
BWRAP_ARGS+=(--bind "$_src" "$_src")
|
|
done
|
|
unset _sub _src
|
|
```
|
|
Mirror this in the dry-run block (after line 555, before line 557 `--ro-bind .gitconfig`):
|
|
```bash
|
|
for _dry_sub in "${MOUNT_HOME[@]}"; do
|
|
_dry_src="$HOME/$_dry_sub"
|
|
[[ -e "$_dry_src" ]] || continue
|
|
echo " --bind $_dry_src $_dry_src \\"
|
|
done
|
|
unset _dry_sub _dry_src
|
|
```
|
|
|
|
3. **SANDBOX_CMD construction**. Replace lines 492-496:
|
|
```bash
|
|
if [[ "$SHELL_MODE" == true ]]; then
|
|
SANDBOX_CMD=("$SANDBOX_BASH" "${CLAUDE_ARGS[@]}")
|
|
elif [[ "$IS_DEFAULT_CLAUDE" == true ]]; then
|
|
SANDBOX_CMD=("$HARNESS_BIN" --dangerously-skip-permissions "${CLAUDE_ARGS[@]}")
|
|
else
|
|
SANDBOX_CMD=("$HARNESS_BIN" "${CLAUDE_ARGS[@]}")
|
|
fi
|
|
```
|
|
|
|
4. **Audit display additions** in `print_audit()` (lines 411-470):
|
|
- At the very top of the function (before the `=== Sandbox Environment ===` header), add a config block when configs were loaded:
|
|
```bash
|
|
if (( ${#CONFIG_FILES_LOADED[@]} > 0 )); then
|
|
echo "${BOLD}${CYAN}=== Config ===${RESET}" >&2
|
|
for _cf in "${CONFIG_FILES_LOADED[@]}"; do
|
|
echo " loaded: $_cf" >&2
|
|
done
|
|
echo " cmd=$HARNESS_CMD ($HARNESS_BIN)" >&2
|
|
(( ${#MOUNT_HOME[@]} > 0 )) && echo " mount_home: ${MOUNT_HOME[*]}" >&2
|
|
(( ${#PATH_ADD[@]} > 0 )) && echo " path_add: ${PATH_ADD[*]}" >&2
|
|
echo "" >&2
|
|
unset _cf
|
|
fi
|
|
```
|
|
Even when no config file is loaded but `HARNESS_CMD != claude` (set via `--cmd`), still show the cmd line so users know they're not running stock claude. Adjust the condition: show the section if `${#CONFIG_FILES_LOADED[@]} > 0 || $IS_DEFAULT_CLAUDE != true`.
|
|
- In the Mounts section (lines 439-463), after the credentials/SSH blocks, add:
|
|
```bash
|
|
for _sub in "${MOUNT_HOME[@]}"; do
|
|
_src="$HOME/$_sub"
|
|
[[ -e "$_src" ]] || continue
|
|
printf ' %-12s %s (read-write, mount_home)\n' "home" "$_src" >&2
|
|
done
|
|
unset _sub _src
|
|
```
|
|
- PATH addition is already visible in the audit because we mutated `SANDBOX_PATH` in step 1; no further change needed (the per-entry display at lines 419-422 will show prepended dirs first).
|
|
|
|
5. Sanity-check shellcheck cleanliness — `writeShellApplication` runs shellcheck at build. Common gotchas:
|
|
- Empty array expansion under `set -u`: guard with `(( ${#arr[@]} > 0 ))` or use `"${arr[@]:-}"`
|
|
- Unused vars when arrays are empty: prefix with `_` or use `unset` after the loop
|
|
- `local` only valid inside functions
|
|
|
|
Do not modify mount logic for existing `~/.claude`, history, SANDBOX.md, credentials, or SSH paths. Do not add any package to `runtimeDeps` in `flake.nix` — pure bash only.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/toph/code/tools/claudebox && nix build .#claudebox 2>&1 | tail -5 && mkdir -p /tmp/le7-test && printf 'cmd = bash\nmount_home = .config\npath_add = ~/.local/bin\n' > /tmp/le7-test/.claudebox && cd /tmp/le7-test && git init -q && /home/toph/code/tools/claudebox/result/bin/claudebox --dry-run --yes 2>&1 | grep -E '(bash|--bind.*\.config|/\.local/bin)' && cd - && rm -rf /tmp/le7-test</automated>
|
|
</verify>
|
|
<done>
|
|
- `nix build .#claudebox` succeeds (shellcheck clean, no runtime errors)
|
|
- Per-project `.claudebox` with `cmd = bash` causes dry-run to show `bash` (not `claude --dangerously-skip-permissions`) as the final exec target
|
|
- `mount_home = .config` produces a `--bind $HOME/.config $HOME/.config` line in dry-run
|
|
- `path_add = ~/.local/bin` causes `~/.local/bin` to appear at the front of the `--setenv PATH` value in dry-run
|
|
- Audit output shows `=== Config ===` block listing loaded files when configs are in play
|
|
- Default invocation (no config, no flags) is byte-identical in behaviour to current claudebox: claude is launched with `--dangerously-skip-permissions`
|
|
- Non-existent `mount_home` subdir produces a warning but does not abort
|
|
- Non-existent `cmd` binary aborts with exit 1 and an error message
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
After both tasks complete, run the full integration check:
|
|
|
|
```bash
|
|
nix build .#claudebox
|
|
|
|
# 1. Default behaviour preserved (no config, no flags)
|
|
./result/bin/claudebox --dry-run --yes 2>&1 | grep -- '--dangerously-skip-permissions' || echo "FAIL: default claude invocation lost"
|
|
|
|
# 2. Per-project .claudebox with custom cmd
|
|
mkdir -p /tmp/le7-int && cd /tmp/le7-int && git init -q
|
|
printf 'cmd = bash\n' > .claudebox
|
|
/home/toph/code/tools/claudebox/result/bin/claudebox --dry-run --yes 2>&1 | tail -3 | grep -v -- '--dangerously-skip-permissions' | grep -q bash && echo "OK: harness override works"
|
|
|
|
# 3. mount_home + path_add
|
|
printf 'cmd = bash\nmount_home = .config\npath_add = ~/.local/bin\n' > .claudebox
|
|
/home/toph/code/tools/claudebox/result/bin/claudebox --dry-run --yes 2>&1 | grep -E '(\.local/bin|--bind.*\.config)' && echo "OK: extras wired"
|
|
|
|
# 4. CLI flag override
|
|
/home/toph/code/tools/claudebox/result/bin/claudebox --cmd /usr/bin/env --dry-run --yes 2>&1 | tail -3
|
|
|
|
# 5. Missing harness binary
|
|
printf 'cmd = totally-nonexistent-binary-xyz\n' > .claudebox
|
|
/home/toph/code/tools/claudebox/result/bin/claudebox --dry-run --yes 2>&1 | grep -i 'not found' && echo "OK: missing binary errors"
|
|
|
|
# 6. --check reports config files
|
|
rm .claudebox
|
|
/home/toph/code/tools/claudebox/result/bin/claudebox --check 2>&1 | grep -E '\.claudebox(/config)?'
|
|
|
|
cd - && rm -rf /tmp/le7-int
|
|
```
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- `nix build .#claudebox` succeeds (shellcheck clean)
|
|
- Default invocation unchanged: `claude --dangerously-skip-permissions` still launched when no config / no `--cmd`
|
|
- `cmd = <other>` config or `--cmd <other>` launches that binary WITHOUT `--dangerously-skip-permissions`
|
|
- Missing harness binary aborts with error+exit 1
|
|
- `mount_home` entries become `--bind` in BWRAP_ARGS and dry-run; missing subdirs warn but do not abort
|
|
- `path_add` entries are prepended to SANDBOX_PATH and visible in audit + dry-run
|
|
- Cascade order respected: `~/.claudebox/config` loaded first, `<CANONICAL_ROOT>/.claudebox` second; CLI flags applied last
|
|
- Audit shows `=== Config ===` section listing loaded files, harness cmd, and accumulated arrays when relevant
|
|
- `--check` reports presence/absence of both config file paths
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/quick/260505-le7-add-harness-config-file-support-to-claud/260505-le7-SUMMARY.md` documenting:
|
|
- Final claudebox.sh structure (where config loading lives, ordering rationale)
|
|
- Any deviations from the plan
|
|
- Manual smoke test result with a real harness (`gsd` if available)
|
|
</output>
|