Close the blockers from the fifth audit
Build App (Preview) / compute-version (pull_request) Successful in 3s
Build Container / build-container (pull_request) Successful in 10m5s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 4m31s
Build App (Preview) / build-linux (pull_request) Successful in 5m21s
Build App (Preview) / build-windows (pull_request) Successful in 19m1s
Build App (Preview) / prune-previews (pull_request) Successful in 1s

Docs and disclosure. HOW-TO-USE.md's settings table still described the
pre-fix behaviour — and help_commands.rs fetches that file from GitHub
main at runtime, ahead of the embedded copy, so it would have reached
every user's Help dialog the moment this merged. The Config tab named
three settings that need a base-image update; there are four, and the
omitted one (Session recap) is the one that fails *without* the "won't
switch off" symptom the warning teaches. Both now also state the cost
nobody had written down: changing any of these recreates the container,
which commits a layer.

Two stale comments that told a reviewer the code was safe when it was
not. compute_claude_code_settings_fingerprint still claimed the
historical fingerprint is preserved so an upgrade cannot churn every
container — carried over from before the widening, false since the
format string changed. And capabilities/default.json, which is the
reviewed threat model of record, described a "Save to host…" action this
branch deletes.

Security and correctness. update_settings validated env vars and nothing
else, so the *global* default_ssh_key_path — the fallback for every
project without an override — took `/` and read-only bind-mounted the
host, which entrypoint.sh then copies into the home volume. classify_
mount_source ran canonicalize on the raw string, which resolves a
relative path against Triple-C's own cwd, so `.` and `..` were accepted
or refused depending on where the app was launched; the daemon then
refuses the mount and the project can never start. Its test passed only
because its examples did not exist under app/src-tauri.

bind_mount_exclusions still derived a path from every row while
project_path_mounts had learned to skip unmountable ones, so a legacy
row made /workspace/<name> ordinary container content that a migration
would then exclude from staging and destroy. The skip is also logged now
rather than silently dropping a folder.

The terminal's file-in path checked is_dir() but not file type, so a
dropped FIFO blocked forever with no timeout — and it is the only route
in now. The web terminal labelled sessions from a global set at request
time, so two quick opens swapped them; harmless until Shift+Enter became
type-dependent, at which point a mislabelled Claude session submitted a
half-written prompt. Opened now carries the type.

Every ~/.claude.json write goes through one atomic helper. The
awsAuthRefresh branches still truncated in place — the same corruption
the Shift+Enter block was fixed for twenty lines later, and its own
comment said so. Demonstrated: a failed write now leaves the original
byte-identical.

And the registration test I added yesterday could pass while the
property was false: an audit got five real unregistered commands past its
exact-string attribute match, and "exactly once" was in its name but not
its body. Mutation-checked against all six shapes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
This commit is contained in:
2026-08-23 18:45:11 -07:00
co-authored by Claude Opus 5
parent 4d1a5a2417
commit 016de8f641
16 changed files with 339 additions and 77 deletions
+33 -33
View File
@@ -454,27 +454,48 @@ fi
# previous Bedrock-profile session — ~/.claude.json lives in the persisted home
# volume, so without this the container keeps trying to run the SSO refresh even
# after switching to a non-SSO backend (Anthropic/Ollama) or to static creds.
# Replace ~/.claude.json atomically: write a sibling temp file, then rename.
#
# `> "$CLAUDE_JSON"` truncates before it writes, so a write that fails part-way
# — a full home volume being the obvious way, and bounding that volume is what
# half this release is about — leaves the file unparseable. It holds the OAuth
# account, and the damage does not self-heal: the next start's `jq` fails on the
# corrupt file, `MERGED` comes back empty, and the `[ -n "$MERGED" ]` guard
# skips the write that would have repaired it. `triple-c-task-runner` has done
# it this way all along.
write_claude_json() {
_wcj_tmp="${CLAUDE_JSON}.triple-c-tmp"
if printf '%s\n' "$1" > "$_wcj_tmp" 2>/dev/null; then
mv -f "$_wcj_tmp" "$CLAUDE_JSON" 2>/dev/null || rm -f "$_wcj_tmp"
else
rm -f "$_wcj_tmp"
echo "entrypoint: warning — could not write $CLAUDE_JSON (leaving it as it was)"
return 1
fi
# By name, after the rename, so these land on the new inode.
chown claude:claude "$CLAUDE_JSON"
chmod 600 "$CLAUDE_JSON"
}
CLAUDE_JSON="/home/claude/.claude.json"
if [ -n "$AWS_SSO_AUTH_REFRESH_CMD" ]; then
if [ -f "$CLAUDE_JSON" ]; then
MERGED=$(jq --arg cmd "$AWS_SSO_AUTH_REFRESH_CMD" '.awsAuthRefresh = $cmd' "$CLAUDE_JSON" 2>/dev/null)
if [ -n "$MERGED" ]; then
printf '%s\n' "$MERGED" > "$CLAUDE_JSON"
write_claude_json "$MERGED"
fi
else
printf '{"awsAuthRefresh":"%s"}\n' "$AWS_SSO_AUTH_REFRESH_CMD" > "$CLAUDE_JSON"
# No existing file, so there is nothing to destroy — but go through the
# same helper so the owner and mode are set in one place.
write_claude_json "$(printf '{"awsAuthRefresh":"%s"}' "$AWS_SSO_AUTH_REFRESH_CMD")"
fi
chown claude:claude "$CLAUDE_JSON"
chmod 600 "$CLAUDE_JSON"
unset AWS_SSO_AUTH_REFRESH_CMD
elif [ -f "$CLAUDE_JSON" ] && grep -q '"awsAuthRefresh"' "$CLAUDE_JSON" 2>/dev/null; then
# Only rewrite when the key is actually present, to avoid a needless jq
# reformat of ~/.claude.json on every start of a non-SSO backend.
MERGED=$(jq 'del(.awsAuthRefresh)' "$CLAUDE_JSON" 2>/dev/null)
if [ -n "$MERGED" ]; then
printf '%s\n' "$MERGED" > "$CLAUDE_JSON"
chown claude:claude "$CLAUDE_JSON"
chmod 600 "$CLAUDE_JSON"
write_claude_json "$MERGED"
fi
fi
@@ -491,38 +512,17 @@ if [ -f "$CLAUDE_JSON" ]; then
# Only rewrite when the value isn't already true, to avoid a needless jq
# reformat of ~/.claude.json on every single start.
if ! grep -q '"shiftEnterKeyBindingInstalled"[[:space:]]*:[[:space:]]*true' "$CLAUDE_JSON" 2>/dev/null; then
# Write to a temp file and rename, never `> "$CLAUDE_JSON"`.
#
# `>` truncates before it writes, so a write that fails part-way — a
# full home volume is the obvious way, and bounding that volume is
# what half this release is about — leaves the file unparseable. This
# file holds the OAuth account, and the damage does not self-heal: the
# next start's `jq` fails on the corrupt file, `MERGED` is empty, and
# the guard below skips the write that would have repaired it. So the
# failure mode is a permanently lost login, for a purely cosmetic flag
# that suppresses a "run /terminal-setup" tip.
#
# `triple-c-task-runner` already does it this way; this block was
# modelled on the awsAuthRefresh one above, which has the same flaw but
# only fires when a Bedrock SSO command is configured.
# Atomic, via `write_claude_json` — see its comment for why a plain
# `>` on this file can permanently destroy the OAuth login.
MERGED=$(jq '.shiftEnterKeyBindingInstalled = true' "$CLAUDE_JSON" 2>/dev/null)
if [ -n "$MERGED" ]; then
CLAUDE_JSON_TMP="${CLAUDE_JSON}.triple-c-tmp"
if printf '%s\n' "$MERGED" > "$CLAUDE_JSON_TMP" 2>/dev/null; then
mv -f "$CLAUDE_JSON_TMP" "$CLAUDE_JSON" 2>/dev/null || rm -f "$CLAUDE_JSON_TMP"
else
# Out of space, or the volume went read-only. The original is
# untouched, which is the whole point.
rm -f "$CLAUDE_JSON_TMP"
echo "entrypoint: warning — could not set shiftEnterKeyBindingInstalled (leaving ~/.claude.json as it was)"
fi
write_claude_json "$MERGED"
fi
fi
else
printf '{"shiftEnterKeyBindingInstalled":true}\n' > "$CLAUDE_JSON"
# Nothing to destroy, but the helper owns the owner/mode too.
write_claude_json '{"shiftEnterKeyBindingInstalled":true}'
fi
chown claude:claude "$CLAUDE_JSON"
chmod 600 "$CLAUDE_JSON"
# ── Docker socket permissions ────────────────────────────────────────────────
if [ -S /var/run/docker.sock ]; then