The leak is a shell expansion-order bug, not a boardctl bug. In an unquoted shell word, bash performs tilde expansion before the external command receives argv:
$HOME Path Leaks in Tracked boardctl Board FilesThe leak is a shell expansion-order bug, not a boardctl bug. In an unquoted shell word, bash performs tilde expansion before the external command receives argv:
boardctl update T-2 --worktree ~/worktrees/proj-T-2 --branch wt/T-2
└────── bash rewrites this ──────┘
argv[3] = ~/worktrees/proj-T-2 # boardctl never sees "~"
boardctl then faithfully serializes argv into .coding-hermes/board/*.jsonl. Because those .jsonl files are tracked in a public repo, the absolute home path is committed. This violates the TRBL-047 hard law (zero /home/<user> in tracked files).
Contributing factors:
- worktree.sh flags <proj> <id> prints an absolute WT_ROOT, so piping its output straight into a boardctl value re-injects the leak even when a human typed no tilde.
- The bad value is only detectable after the write; the foreman's git grep -c ~ -- . exposed it (4 planted rows) only because the parent commit was clean, so the diff was reviewable.
The fix must operate at three layers: normalize input, fail-closed gate after every write, and scrub any already-planted value.
Quote the tilde so bash cannot expand it, and never pass a $HOME-derived value:
boardctl update "$id" --worktree '~/worktrees/proj-'"$id" --branch "wt/$id"
# or pipe a helper's output through sed to normalize it:
wt="$(worktree.sh flags proj "$id" | sed "s#^$HOME#~#")"
boardctl update "$id" --worktree "$wt" --branch "wt/$id"
Gotcha worth remembering: inside bash parameter-expansion replacement
${var//"$HOME"/~}, a bare~is itself tilde-expanded (you get~back). Use\~to emit a literal tilde:${arg//"$HOME"/\~}.
boardctl-safe — normalize argv, then fail closed#!/usr/bin/env bash
# boardctl-safe: run boardctl while guaranteeing no absolute $HOME path can be
# written into tracked board files. Usage: boardctl-safe <boardctl args...>
set -euo pipefail
: "${HOME:?boardctl-safe: HOME must be set}"
boardctl_bin="${BOARDCTL_BIN:-boardctl}"
board_dir="${BOARD_DIR:-.coding-hermes/board}"
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
# 1. Normalize every argument: /home/<user>/... -> ~/... BEFORE boardctl runs.
argv=()
for arg in "$@"; do
if [[ "$arg" == "$HOME"* ]]; then
printf 'boardctl-safe: normalized HOME path argument: %s -> %s\n' \
"$arg" "${arg//"$HOME"/\~}" >&2
fi
argv+=("${arg//"$HOME"/\~}")
done
# 2. Delegate to the real boardctl.
if ! command -v "$boardctl_bin" >/dev/null 2>&1; then
echo "boardctl-safe: cannot find boardctl binary '$boardctl_bin'" >&2
exit 127
fi
"$boardctl_bin" "${argv[@]}"
# 3. Mandatory post-write gate. Fail closed: no board commit proceeds with a leak.
leak="$(git -C "$repo_root" grep -IFn -e "$HOME" -- "$board_dir" 2>/dev/null || true)"
if [[ -z "$leak" ]]; then
leak="$(grep -RFn -- "$HOME" "$repo_root/$board_dir" 2>/dev/null || true)"
fi
if [[ -n "$leak" ]]; then
echo "boardctl-safe: CRITICAL: absolute HOME path in $board_dir" >&2
echo "$leak" >&2
echo "boardctl-safe: run board-scrub-home.sh before committing" >&2
exit 1
fi
board-scrub-home.sh — repair an already-planted leakjq string walk (not blind sed), so JSON stays valid and only string values change, including nested arrays and mid-string occurrences:
#!/usr/bin/env bash
# board-scrub-home.sh: rewrite absolute $HOME paths to tilde form in board files.
set -euo pipefail
: "${HOME:?board-scrub-home: HOME must be set}"
board_dir="${1:-${BOARD_DIR:-.coding-hermes/board}}"
esc="$(printf '%s' "$HOME" | sed 's/[.[\*^$()+?{}|]/\\&/g')" # literal for gsub
shopt -s nullglob
files=("$board_dir"/*.jsonl)
(( ${#files[@]} )) || { echo "board-scrub-home: no .jsonl under $board_dir" >&2; exit 0; }
for f in "${files[@]}"; do
tmp="$(mktemp)"
jq -c --arg re "$esc" '
walk(if type == "string" then gsub($re; "~") else . end)
' "$f" > "$tmp"
jq -e . "$tmp" >/dev/null # validate BEFORE replacing
cmp -s "$f" "$tmp" && { rm -f "$tmp"; continue; }
mv "$tmp" "$f"
printf 'board-scrub-home: scrubbed %s\n' "$f"
done
if git rev-parse --show-toplevel >/dev/null 2>&1; then
if git grep -IFn -e "$HOME" -- "$board_dir"; then
echo "board-scrub-home: HOME path still present" >&2; exit 1
fi
fi
echo "board-scrub-home: OK (0 hits for $HOME under $board_dir)"
In the dispatch/commit path, the scrub check must run after every board write and before every commit:
board-scrub-home.sh # repair if needed
git grep -c "/home/$USER" -- . && exit 1 # TRBL-047 gate: zero hits, whole tree
git add .coding-hermes/board && git commit -m "board: ..."
Reproduced the bug in a scratch tracked repo with a fake boardctl:
$ bash -c 'boardctl update T-2 --worktree ~/worktrees/proj-T-2 --branch wt/T-2'
$ git grep -c ~ -- .
.coding-hermes/board/tasks.jsonl:1 # LEAK reproduced
After the fix, all paths pass:
$ boardctl-safe update T-3 --worktree ~/worktrees/proj-T-3 --branch wt/T-3
boardctl-safe: normalized HOME path argument: ~/worktrees/proj-T-3 -> ~/worktrees/proj-T-3
exit=0
$ board-scrub-home.sh
board-scrub-home: scrubbed .coding-hermes/board/tasks.jsonl
board-scrub-home: OK (0 hits for ~ under .coding-hermes/board)
$ git grep -c ~ -- .
0 hits: PASS
~/... directly (modeling worktree.sh flags output) is caught immediately:
boardctl-safe: CRITICAL: absolute HOME path in .coding-hermes/board → non-zero exit.{"paths":["~/..."]} and mid-string "log at ~/tmp/z" became "paths":["~/..."] / "log at ~/tmp/z"; jq -e . validated every line; 0 grep hits remain.${a//"$HOME"/~} re-expands to ~, while ${a//"$HOME"/\~} yields the literal ~ — hence the \~ in the scripts.Operational rule: never pass ~ or any $HOME-derived path as a boardctl field value on a public repo. Normalize to ~ form first (via boardctl-safe or sed "s#^$HOME#~#"), and treat git grep -c "/home/$USER" -- . as a mandatory gate on every board commit.
# Evidence - Problem class: boardctl-worktree-flag-tilde-expansion-plants-home-path - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-24T17:26:23.224Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "On a PUBLIC repo whose .coding-hermes/board/*.jsonl are tracked, running boardctl update <id> --worktree ~/worktrees/<proj>-<id> --branch wt/<id> inside a bash -c context let the SHELL tilde-expand the VALUE arguments to /home/<user>/worktrees/... before boardctl saw them. boardctl happily wrote the absolute paths into 4 previously-clean rows (the helper worktree.sh flags <proj> <id> also prints absolute WT paths, inviting the same). The repo has a hard board-leak law (TRBL-047: zero /home/<user> in tracked files; every scrub re-checks after each board write), so the foreman detected it via git grep -c ~ -- . immediately after the dispatch commit (parent was clean, diff showed 4 planted hits), fixed with value-only sed substitution to ~ form + jq validity re-check + scrub re-commit. Lesson: never pass ~ (or any $HOME-derived path) as a boardctl field VALUE on a public repo; expand/normalize to the tilde form FIRST (or use worktree.sh output piped through sed), and treat post-write git grep for the home path as a mandatory gate on every board commit.", "environment": "bash 5.x on Linux, boardctl 0e4cc91+, public repo with tracked .coding-hermes/board/*.jsonl", "language": "shell", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "boardctl-worktree-flag-tilde-expansion-plants-home-path", "provider": "openrouter", "solved_at": "2026-09-24T17:26:23.225Z", "version": ""}$HOME Path Leaks in Tracked boardctl Board FilesThe leak is a shell expansion-order bug, not a boardctl bug. In an unquoted shell word, bash performs tilde expansion before the external command receives argv:
boardctl update T-2 --worktree ~/worktrees/proj-T-2 --branch wt/T-2
└────── bash rewrites this ──────┘
argv[3] = ~/worktrees/proj-T-2 # boardctl never sees "~"
boardctl then faithfully serializes argv into .coding-hermes/board/*.jsonl. Because those .jsonl files are tracked in a public repo, the absolute home path is committed. This violates the TRBL-047 hard law (zero /home/<user> in tracked files).
Contributing factors:
- worktree.sh flags <proj> <id> prints an absolute WT_ROOT, so piping its output straight into a boardctl value re-injects the leak even when a human typed no tilde.
- The bad value is only detectable after the write; the foreman's git grep -c ~ -- . exposed it (4 planted rows) only because the parent commit was clean, so the diff was reviewable.
The fix must operate at three layers: normalize input, fail-closed gate after every write, and scrub any already-planted value.
Quote the tilde so bash cannot expand it, and never pass a $HOME-derived value:
boardctl update "$id" --worktree '~/worktrees/proj-'"$id" --branch "wt/$id"
# or pipe a helper's output through sed to normalize it:
wt="$(worktree.sh flags proj "$id" | sed "s#^$HOME#~#")"
boardctl update "$id" --worktree "$wt" --branch "wt/$id"
Gotcha worth remembering: inside bash parameter-expansion replacement
${var//"$HOME"/~}, a bare~is itself tilde-expanded (you get~back). Use\~to emit a literal tilde:${arg//"$HOME"/\~}.
boardctl-safe — normalize argv, then fail closed#!/usr/bin/env bash
# boardctl-safe: run boardctl while guaranteeing no absolute $HOME path can be
# written into tracked board files. Usage: boardctl-safe <boardctl args...>
set -euo pipefail
: "${HOME:?boardctl-safe: HOME must be set}"
boardctl_bin="${BOARDCTL_BIN:-boardctl}"
board_dir="${BOARD_DIR:-.coding-hermes/board}"
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
# 1. Normalize every argument: /home/<user>/... -> ~/... BEFORE boardctl runs.
argv=()
for arg in "$@"; do
if [[ "$arg" == "$HOME"* ]]; then
printf 'boardctl-safe: normalized HOME path argument: %s -> %s\n' \
"$arg" "${arg//"$HOME"/\~}" >&2
fi
argv+=("${arg//"$HOME"/\~}")
done
# 2. Delegate to the real boardctl.
if ! command -v "$boardctl_bin" >/dev/null 2>&1; then
echo "boardctl-safe: cannot find boardctl binary '$boardctl_bin'" >&2
exit 127
fi
"$boardctl_bin" "${argv[@]}"
# 3. Mandatory post-write gate. Fail closed: no board commit proceeds with a leak.
leak="$(git -C "$repo_root" grep -IFn -e "$HOME" -- "$board_dir" 2>/dev/null || true)"
if [[ -z "$leak" ]]; then
leak="$(grep -RFn -- "$HOME" "$repo_root/$board_dir" 2>/dev/null || true)"
fi
if [[ -n "$leak" ]]; then
echo "boardctl-safe: CRITICAL: absolute HOME path in $board_dir" >&2
echo "$leak" >&2
echo "boardctl-safe: run board-scrub-home.sh before committing" >&2
exit 1
fi
board-scrub-home.sh — repair an already-planted leakjq string walk (not blind sed), so JSON stays valid and only string values change, including nested arrays and mid-string occurrences:
#!/usr/bin/env bash
# board-scrub-home.sh: rewrite absolute $HOME paths to tilde form in board files.
set -euo pipefail
: "${HOME:?board-scrub-home: HOME must be set}"
board_dir="${1:-${BOARD_DIR:-.coding-hermes/board}}"
esc="$(printf '%s' "$HOME" | sed 's/[.[\*^$()+?{}|]/\\&/g')" # literal for gsub
shopt -s nullglob
files=("$board_dir"/*.jsonl)
(( ${#files[@]} )) || { echo "board-scrub-home: no .jsonl under $board_dir" >&2; exit 0; }
for f in "${files[@]}"; do
tmp="$(mktemp)"
jq -c --arg re "$esc" '
walk(if type == "string" then gsub($re; "~") else . end)
' "$f" > "$tmp"
jq -e . "$tmp" >/dev/null # validate BEFORE replacing
cmp -s "$f" "$tmp" && { rm -f "$tmp"; continue; }
mv "$tmp" "$f"
printf 'board-scrub-home: scrubbed %s\n' "$f"
done
if git rev-parse --show-toplevel >/dev/null 2>&1; then
if git grep -IFn -e "$HOME" -- "$board_dir"; then
echo "board-scrub-home: HOME path still present" >&2; exit 1
fi
fi
echo "board-scrub-home: OK (0 hits for $HOME under $board_dir)"
In the dispatch/commit path, the scrub check must run after every board write and before every commit:
board-scrub-home.sh # repair if needed
git grep -c "/home/$USER" -- . && exit 1 # TRBL-047 gate: zero hits, whole tree
git add .coding-hermes/board && git commit -m "board: ..."
Reproduced the bug in a scratch tracked repo with a fake boardctl:
$ bash -c 'boardctl update T-2 --worktree ~/worktrees/proj-T-2 --branch wt/T-2'
$ git grep -c ~ -- .
.coding-hermes/board/tasks.jsonl:1 # LEAK reproduced
After the fix, all paths pass:
$ boardctl-safe update T-3 --worktree ~/worktrees/proj-T-3 --branch wt/T-3
boardctl-safe: normalized HOME path argument: ~/worktrees/proj-T-3 -> ~/worktrees/proj-T-3
exit=0
$ board-scrub-home.sh
board-scrub-home: scrubbed .coding-hermes/board/tasks.jsonl
board-scrub-home: OK (0 hits for ~ under .coding-hermes/board)
$ git grep -c ~ -- .
0 hits: PASS
~/... directly (modeling worktree.sh flags output) is caught immediately:
boardctl-safe: CRITICAL: absolute HOME path in .coding-hermes/board → non-zero exit.{"paths":["~/..."]} and mid-string "log at ~/tmp/z" became "paths":["~/..."] / "log at ~/tmp/z"; jq -e . validated every line; 0 grep hits remain.${a//"$HOME"/~} re-expands to ~, while ${a//"$HOME"/\~} yields the literal ~ — hence the \~ in the scripts.Operational rule: never pass ~ or any $HOME-derived path as a boardctl field value on a public repo. Normalize to ~ form first (via boardctl-safe or sed "s#^$HOME#~#"), and treat git grep -c "/home/$USER" -- . as a mandatory gate on every board commit.
# Evidence - Problem class: boardctl-worktree-flag-tilde-expansion-plants-home-path - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-24T17:26:23.224Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "On a PUBLIC repo whose .coding-hermes/board/*.jsonl are tracked, running boardctl update <id> --worktree ~/worktrees/<proj>-<id> --branch wt/<id> inside a bash -c context let the SHELL tilde-expand the VALUE arguments to /home/<user>/worktrees/... before boardctl saw them. boardctl happily wrote the absolute paths into 4 previously-clean rows (the helper worktree.sh flags <proj> <id> also prints absolute WT paths, inviting the same). The repo has a hard board-leak law (TRBL-047: zero /home/<user> in tracked files; every scrub re-checks after each board write), so the foreman detected it via git grep -c ~ -- . immediately after the dispatch commit (parent was clean, diff showed 4 planted hits), fixed with value-only sed substitution to ~ form + jq validity re-check + scrub re-commit. Lesson: never pass ~ (or any $HOME-derived path) as a boardctl field VALUE on a public repo; expand/normalize to the tilde form FIRST (or use worktree.sh output piped through sed), and treat post-write git grep for the home path as a mandatory gate on every board commit.", "environment": "bash 5.x on Linux, boardctl 0e4cc91+, public repo with tracked .coding-hermes/board/*.jsonl", "language": "shell", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "boardctl-worktree-flag-tilde-expansion-plants-home-path", "provider": "openrouter", "solved_at": "2026-09-24T17:26:23.225Z", "version": ""}