18 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 260505-le7 | 01 | execute | 1 |
|
true |
|
|
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.
<execution_context> @/home/toph/code/tools/claudebox/.claude/get-shit-done/workflows/execute-plan.md </execution_context>
@/home/toph/code/tools/claudebox/CLAUDE.md @/home/toph/code/tools/claudebox/claudebox.sh @/home/toph/code/tools/claudebox/flake.nixCurrent 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>/.claudeboxHOME,SANDBOX_PATH(injected by flake.nix),SANDBOX_BASH,CLAUDE_BIN
Existing parallel pattern to mimic — env files (lines 386-408):
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):
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.
-
Initialise globals near the top (alongside
SKIP_AUDIT=falseetc., line 1-9):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 -
Add CLI flags to the while-loop (lines 11-28):
--cmd <binary>→ setsHARNESS_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-keypattern at lines 19-22).
-
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: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" } -
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_ROOTcomputation earlier (right afterCWD=$(pwd)at line 178), so it is available before config loading. Verify nothing earlier in the script depends onCWDhaving NOT been canonicalised — it doesn't, onlyCWDitself is used. - Then load configs in this order (cascading, later overrides earlier scalar / appends to arrays):
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:
Note the bash gotcha:[[ -n "$CLI_HARNESS_CMD" ]] && HARNESS_CMD="$CLI_HARNESS_CMD" MOUNT_HOME+=("${CLI_MOUNT_HOME[@]}") PATH_ADD+=("${CLI_PATH_ADD[@]}")MOUNT_HOME+=("${CLI_MOUNT_HOME[@]}")errors underset -uwhen the array is empty. UseMOUNT_HOME+=("${CLI_MOUNT_HOME[@]:-}")guarded by length check, or(( ${#CLI_MOUNT_HOME[@]} > 0 )) && MOUNT_HOME+=("${CLI_MOUNT_HOME[@]}").
- Move
-
Resolve harness binary. After
CLAUDE_BIN="$(command -v claude)"(line 175), add: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=trueNote 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.
-
--checkadditions: in the CHECK_MODE block (lines 71-112), after the~/.claudeboxcheck, add: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 ficompute_canonical_rootis 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, alongsidegc_instances).
Do not introduce new external dependencies. Keep everything in pure bash. Preserve set -euo pipefail semantics imposed by writeShellApplication.
cd /home/toph/code/tools/claudebox && nix build .#claudebox 2>&1 | tail -20 && ./result/bin/claudebox --check 2>&1 | grep -E '(claudebox/config|/.claudebox)'
- New flags
--cmd,--mount-home,--path-addparse without error nix build .#claudeboxsucceeds (shellcheck clean)--checkreports presence/absence of both config file paths- HARNESS_BIN resolves correctly; missing harness binary produces error+exit 1
IS_DEFAULT_CLAUDEflag correctly distinguishes default-claude from override
-
PATH augmentation. BEFORE building
ENV_ARGS(currently at line 320), prepend PATH_ADD entries to SANDBOX_PATH: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 fiThis mutates SANDBOX_PATH before it is consumed by
--setenv PATH "$SANDBOX_PATH"and byAUDIT_SANDBOX_VALS[PATH]— both will reflect the prepended dirs automatically. Verify by inspecting lines 324 and 333 use$SANDBOX_PATHdirectly. -
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):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 _srcMirror this in the dry-run block (after line 555, before line 557
--ro-bind .gitconfig):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 -
SANDBOX_CMD construction. Replace lines 492-496:
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 -
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:
Even when no config file is loaded butif (( ${#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 fiHARNESS_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:
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_PATHin step 1; no further change needed (the per-entry display at lines 419-422 will show prepended dirs first).
- At the very top of the function (before the
-
Sanity-check shellcheck cleanliness —
writeShellApplicationruns 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 useunsetafter the loop localonly valid inside functions
- Empty array expansion under
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.
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
nix build .#claudeboxsucceeds (shellcheck clean, no runtime errors)- Per-project
.claudeboxwithcmd = bashcauses dry-run to showbash(notclaude --dangerously-skip-permissions) as the final exec target mount_home = .configproduces a--bind $HOME/.config $HOME/.configline in dry-runpath_add = ~/.local/bincauses~/.local/binto appear at the front of the--setenv PATHvalue 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_homesubdir produces a warning but does not abort - Non-existent
cmdbinary aborts with exit 1 and an error message
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
<success_criteria>
nix build .#claudeboxsucceeds (shellcheck clean)- Default invocation unchanged:
claude --dangerously-skip-permissionsstill 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_homeentries become--bindin BWRAP_ARGS and dry-run; missing subdirs warn but do not abortpath_addentries are prepended to SANDBOX_PATH and visible in audit + dry-run- Cascade order respected:
~/.claudebox/configloaded first,<CANONICAL_ROOT>/.claudeboxsecond; CLI flags applied last - Audit shows
=== Config ===section listing loaded files, harness cmd, and accumulated arrays when relevant --checkreports presence/absence of both config file paths </success_criteria>