Skip to content
Merged
34 changes: 34 additions & 0 deletions bin/backends/cmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,27 @@ fm_backend_cmux_surface_id_for_workspace() { # <workspace_id>
| jq -r '.panes[0] // {} | .selected_surface_id // (.surface_ids[0] // empty)' 2>/dev/null
}

# fm_backend_cmux_workspace_matches_label: does <workspace_id> still carry the
# workspace title firstmate expects for caller-facing task label <label>?
# Looked up by exact id (never falls back to searching by label alone the
# way fm_backend_cmux_target_ready does for its reconnect purpose), so a
# workspace id that no longer resolves at all is a refusal, not a signal to
# go find some other live workspace that happens to share the title.
# Mirrors fm_backend_zellij_tab_matches_label's scoped/bare-with-ambiguity
# rules: checks the home-scoped title first, then the legacy bare label, but
# only when the bare title is not ambiguous across live workspaces.
fm_backend_cmux_workspace_matches_label() { # <workspace_id> <label>
local workspace_id=$1 label=$2 scoped workspaces count
scoped=$(fm_backend_cmux_scoped_title "$label")
workspaces=$(fm_backend_cmux_cli workspace list --json --id-format uuids 2>/dev/null)
printf '%s' "$workspaces" | jq -e --arg id "$workspace_id" --arg want "$scoped" \
'[.workspaces[]? | select(.id == $id and .title == $want)] | length > 0' >/dev/null 2>&1 && return 0
printf '%s' "$workspaces" | jq -e --arg id "$workspace_id" --arg want "$label" \
'[.workspaces[]? | select(.id == $id and .title == $want)] | length > 0' >/dev/null 2>&1 || return 1
count=$(printf '%s' "$workspaces" | jq -r --arg want "$label" '[.workspaces[]? | select(.title == $want)] | length' 2>/dev/null)
[ "$count" = "1" ]
}

# fm_backend_cmux_create_task: create the task's workspace (one surface),
# refusing an existing live <label> (finding #6: cmux enforces no uniqueness
# itself). Resolves the fresh workspace's default surface via one list-panes
Expand Down Expand Up @@ -403,6 +424,19 @@ fm_backend_cmux_surface_exists() { # <workspace_id> <surface_id>
| jq -e --arg s "$sfid" '[.panes[]? | select(.surface_ids // [] | index($s))] | length > 0' >/dev/null 2>&1
}

# fm_backend_cmux_surface_verifies_task: does <surface_id> still exist inside
# <workspace_id>, and does that exact workspace still carry the task label
# for <task_id>? Mirrors fm_backend_herdr_pane_verifies_task's identity-proof
# role for legacy-metadata self-repair (bin/fm-backend.sh): both the surface
# membership and the workspace's own title must independently hold, or this
# refuses rather than guessing.
fm_backend_cmux_surface_verifies_task() { # <workspace_id> <surface_id> <task_id>
local workspace_id=$1 surface_id=$2 task_id=$3
[ -n "$workspace_id" ] && [ -n "$surface_id" ] && [ -n "$task_id" ] || return 1
fm_backend_cmux_surface_exists "$workspace_id" "$surface_id" || return 1
fm_backend_cmux_workspace_matches_label "$workspace_id" "fm-$task_id"
}

# fm_backend_cmux_target_ready: parse the target and verify it is live via
# fm_backend_cmux_surface_exists (never read-screen - see that function's
# header for the fresh-surface pitfall this avoids). When the caller knows
Expand Down
18 changes: 18 additions & 0 deletions bin/backends/zellij.sh
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,24 @@ fm_backend_zellij_tab_matches_label() { # <session> <tab_id> <label>
[ "$count" = "1" ]
}

# fm_backend_zellij_pane_verifies_task: does <pane_id> in <session> still
# belong to <tab_id>, and does that tab still carry the task label for
# <task_id>? Mirrors fm_backend_herdr_pane_verifies_task's identity-proof
# role for legacy-metadata self-repair (bin/fm-backend.sh): the pane's
# CURRENT owning tab is looked up fresh (never trusts the recorded tab_id
# alone, since a pane can move tabs), then that owning tab must equal the
# recorded tab_id, then the tab's name must match via
# fm_backend_zellij_tab_matches_label's scoped/bare-with-ambiguity-check
# rules. Any mismatch, including a pane that moved tabs or a tab renamed to
# a different task's label, refuses rather than guessing.
fm_backend_zellij_pane_verifies_task() { # <session> <tab_id> <pane_id> <task_id>
local session=$1 tab_id=$2 pane_id=$3 task_id=$4 owning_tab
[ -n "$session" ] && [ -n "$tab_id" ] && [ -n "$pane_id" ] && [ -n "$task_id" ] || return 1
owning_tab=$(fm_backend_zellij_tab_for_pane "$session" "$pane_id") || return 1
[ -n "$owning_tab" ] && [ "$owning_tab" = "$tab_id" ] || return 1
fm_backend_zellij_tab_matches_label "$session" "$tab_id" "fm-$task_id"
}

# fm_backend_zellij_create_task: create the task's tab (one terminal pane) in
# <session>, refusing an existing <label>. Zellij does NOT enforce tab-name
# uniqueness itself (verified: two tabs can share a name), so the duplicate
Expand Down
79 changes: 66 additions & 13 deletions bin/claude-account.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@
# .credentials.json and .claude.json are never in that symlink list - they must
# stay per-account real files or OAuth tokens leak across accounts.
#
# Auth model: this launcher authenticates each account with a long-lived
# `claude setup-token` (a CLAUDE_CODE_OAUTH_TOKEN, sk-ant-oat01-...) resolved
# from the macOS keychain, exported into the session env, and honored by current
# Claude Code interactive sessions. Setup tokens are chosen over the interactive
# .credentials.json / `claude /login` blob on purpose: they are ~1yr-lived and
# never silently log out mid-fleet-run (an expired/cleared .credentials.json
# does), and they are the same tokens the ccjuggler/juggle switcher already
# stores, so all multi-account auth shares one source of truth.
#
# Token store: keychain service "ccjuggler-acc<N>", account "ccjuggler" (the
# exact lookup `juggle`/ccjuggler.py use). Seed or rotate a token with
# `claude setup-token` under the target account, then store it with
# security add-generic-password -U -s "ccjuggler-acc<N>" -a "ccjuggler" -w "<token>"
# (or `juggle add`). The launcher validates the sk-ant-oat01- prefix and refuses
# a missing or malformed token rather than launching into a login prompt.
#
# Onboarding pre-seed location (current Claude Code): when CLAUDE_CONFIG_DIR is
# set, Claude Code reads its global config JSON from $CLAUDE_CONFIG_DIR/.claude.json
# (path = join(CLAUDE_CONFIG_DIR ?? homedir, ".claude.json")), NOT from a
# .claude.json in the PARENT of that dir. The onboarding gate it checks is a
# single key, hasCompletedOnboarding===true; the first-run welcome/theme/login
# flow (and everything under it) is skipped once that is set. We therefore write
# the pre-seed into $CLAUDE_CONFIG_DIR/.claude.json (see below).
#
# flock on a per-account lock file serializes the bootstrap section below so
# two concurrent first launches on the same account cannot race on the JSON
# writes and corrupt .claude.json; the lock fd is closed before exec claude so
Expand All @@ -32,14 +56,30 @@ esac
ACCOUNT_HOME="$HOME/.claude-homes/account${ACCOUNT}"
export CLAUDE_CONFIG_DIR="$ACCOUNT_HOME/.claude"

if [ ! -f "$CLAUDE_CONFIG_DIR/.credentials.json" ]; then
echo "error: credentials not found at $CLAUDE_CONFIG_DIR/.credentials.json" >&2
echo "seed them with:" >&2
echo " CLAUDE_CONFIG_DIR=$CLAUDE_CONFIG_DIR claude /login" >&2
exit 1
fi
# Authenticate with the account's long-lived setup token from the keychain.
# Refuse rather than launch into a login prompt when the token is absent or
# malformed (a corrupt/non-oat value stored here is what silently broke acc2).
KEYCHAIN_SERVICE="ccjuggler-acc${ACCOUNT}"
OAUTH_TOKEN="$(security find-generic-password -s "$KEYCHAIN_SERVICE" -a "ccjuggler" -w 2>/dev/null || true)"
case "$OAUTH_TOKEN" in
sk-ant-oat01-*) ;;
"")
echo "error: no setup token in keychain service '$KEYCHAIN_SERVICE' (account 'ccjuggler')" >&2
echo "seed it once: run 'claude setup-token' under the target account, then:" >&2
echo " security add-generic-password -U -s \"$KEYCHAIN_SERVICE\" -a \"ccjuggler\" -w \"<token>\"" >&2
exit 1 ;;
*)
echo "error: keychain service '$KEYCHAIN_SERVICE' holds a value that is not a setup token" >&2
echo "(expected an sk-ant-oat01-... token). Re-store the correct token with:" >&2
echo " security add-generic-password -U -s \"$KEYCHAIN_SERVICE\" -a \"ccjuggler\" -w \"<token>\"" >&2
exit 1 ;;
esac
export CLAUDE_CODE_OAUTH_TOKEN="$OAUTH_TOKEN"

mkdir -p "$ACCOUNT_HOME"
# Create the per-account config dir (not just the home) so a brand-new account -
# one never seeded by an interactive login - has $CLAUDE_CONFIG_DIR present for
# the symlink and .claude.json pre-seed steps below.
mkdir -p "$CLAUDE_CONFIG_DIR"
exec 9>"$ACCOUNT_HOME/.claude-account.lock"
flock 9

Expand All @@ -53,12 +93,15 @@ for item in commands hooks skills mcp-configs settings.json settings.local.json
fi
done

# .claude.json lives in the PARENT of CLAUDE_CONFIG_DIR - a Claude Code
# convention, not something this pattern invented. Pre-accept onboarding and
# the trust dialog for the working directory so a headless session doesn't
# hang on either prompt; CLAUDE_TRUST_DIR overrides which directory gets
# pre-trusted when it differs from the launcher's own cwd.
CLAUDE_JSON="$ACCOUNT_HOME/.claude.json"
# Pre-accept onboarding and the trust dialog so a session doesn't land in the
# first-run onboarding flow or hang on the trust prompt. When CLAUDE_CONFIG_DIR
# is set, current Claude Code reads its global config from
# $CLAUDE_CONFIG_DIR/.claude.json (NOT a .claude.json in the parent dir, which
# older layouts used and which CC now ignores) - so the pre-seed MUST live
# there or onboarding is not skipped. The onboarding gate is the single key
# hasCompletedOnboarding===true. CLAUDE_TRUST_DIR overrides which directory
# gets pre-trusted when it differs from the launcher's own cwd.
CLAUDE_JSON="$CLAUDE_CONFIG_DIR/.claude.json"
CLAUDE_TRUST_DIR="${CLAUDE_TRUST_DIR:-$PWD}" python3 - "$CLAUDE_JSON" <<'PYEOF'
import json
import os
Expand All @@ -85,6 +128,16 @@ if not projects.get(trust_dir, {}).get("hasTrustDialogAccepted"):
projects.setdefault(trust_dir, {})["hasTrustDialogAccepted"] = True
changed = True

# Auto-approve project-scoped (.mcp.json) MCP servers so a fresh account home
# is not dropped into the per-server "New MCP server found" prompt on first
# encounter. .claude.json is per-account (never symlinked, to avoid OAuth/
# project-state leak), so an account never inherits the primary's MCP approvals
# - this global flag is the durable equivalent of choosing "use this and all
# future MCP servers in this project".
if not data.get("enableAllProjectMcpServers"):
data["enableAllProjectMcpServers"] = True
changed = True
Comment on lines +131 to +139

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)(bin/claude-account\.sh|tests/fm-spawn-account\.test\.sh|docs/configuration\.md|$\.claude\.json|\.mcp\.json)$' || true

echo "== outline =="
ast-grep outline bin/claude-account.sh || true

echo "== relevant source lines =="
cat -n bin/claude-account.sh | sed -n '1,220p'

echo "== docs / config mentions =="
if [ -f docs/configuration.md ]; then rg -n "enableAllProjectMcpServers|enable.*Mcp|project.*mcp|\.mcp\.json" docs/configuration.md || true; fi

echo "== tests/mentions =="
rg -n "enableAllProjectMcpServers|enable.*Mcp|project.*mcp|\.mcp\.json|claude-account|CLAUDE_TRUST_DIR" bin tests docs -S || true

echo "== repo root config files =="
find . -maxdepth 3 \( -name '.claude.json' -o -name '.mcp.json' -o -name 'claude.json' -o -name 'mcp.json' \) -print 2>/dev/null || true

Repository: trillium/firstmate

Length of output: 16134


🌐 Web query:

Claude Code MCP project .mcp.json command enableAllProjectMcpServers documentation

💡 Result:

In Claude Code, enableAllProjectMcpServers is a configuration setting used to automatically approve and enable all MCP servers defined in a project's.mcp.json file, bypassing the individual per-server prompt [1][2][3]. Key points regarding its usage and limitations: 1. Purpose and Usage: By setting "enableAllProjectMcpServers": true in your Claude Code settings (typically in ~/.claude/settings.json), you authorize all project-scoped servers to run without needing to manually approve them each time [2][3]. 2. Important Security Constraint: As of Claude Code v2.1.196, this setting is ignored if the workspace folder is considered "untrusted" [4][1]. Even if "enableAllProjectMcpServers": true is committed to a project's.claude/settings.json file, Claude Code will not honor it until you have explicitly trusted the workspace by running the claude command in that folder and accepting the trust dialog [4][1]. This prevents a potential security risk where a cloned, untrusted repository could automatically execute its own configured servers [4][1]. 3. Recommended Alternative: For better security control, it is often recommended to use the enabledMcpjsonServers setting instead [1]. This allows you to explicitly list only the specific servers you trust by name, rather than using a blanket true setting that enables every server defined in the.mcp.json file [1]. 4. Troubleshooting: If your.mcp.json servers are stuck at "Pending approval," it is frequently because the workspace trust gate has not been cleared [4][1]. Additionally, note that entries in disabledMcpjsonServers will override enableAllProjectMcpServers, meaning a server explicitly disabled will not run regardless of your global "enable all" setting [1].

Citations:


Security Misconfiguration (CWE-829): Inclusion of Functionality from Untrusted Control Sphere

Reachability: Internal

Do not persist enableAllProjectMcpServers=true on account bootstrap.

bin/claude-account.sh writes this global approval into $CLAUDE_CONFIG_DIR/.claude.json, and docs/configuration.md documents it as intentional. Add MCP approval requirements behind an explicit captain opt-in for a known project/server, or keep Claude Code’s default per-server approval. Update docs/configuration.md and the account bootstrap tests with the replacement behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/claude-account.sh` around lines 131 - 139, Remove the unconditional
enableAllProjectMcpServers assignment from the account bootstrap in
bin/claude-account.sh, preserving Claude Code’s default per-server approval
unless an explicit captain opt-in targets a known project/server. Update
docs/configuration.md and the account bootstrap tests to document and verify the
replacement behavior.


if changed:
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path) or ".", prefix=".claude-account.")
try:
Expand Down
41 changes: 33 additions & 8 deletions bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -483,10 +483,6 @@ fm_backend_validate_task_endpoint() { # <meta-file> <task-id>
fi
;;
zellij)
[ "$binding" = "$id" ] || {
echo "REFUSED: legacy Zellij endpoint metadata for task $id lacks an exact task binding; preserving task state." >&2
return 1
}
recorded_session=$(fm_backend_meta_exact_value "$meta" zellij_session) || recorded_session=
tab=$(fm_backend_meta_exact_value "$meta" zellij_tab_id) || tab=
pane=$(fm_backend_meta_exact_value "$meta" zellij_pane_id) || pane=
Expand All @@ -497,8 +493,29 @@ fm_backend_validate_task_endpoint() { # <meta-file> <task-id>
echo "REFUSED: Zellij endpoint metadata for task $id is malformed or inconsistent; preserving task state." >&2
return 1
fi
if [ "$binding" = "$id" ]; then
:
else
fm_backend_source zellij || return 1
if fm_backend_zellij_pane_verifies_task "$recorded_session" "$tab" "$pane" "$id" 2>/dev/null; then
echo "zellij endpoint self-repair: appending endpoint_task_id=$id to metadata for legacy task $id" >&2
printf 'endpoint_task_id=%s\n' "$id" >> "$meta" || return 1
Comment on lines +500 to +502

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve metadata record boundaries during repair.

At Line 502 and Line 556, a metadata file accepted without a final LF causes endpoint_task_id to concatenate with its final field. The function then returns success without creating a binding and corrupts that field.

  • bin/fm-backend.sh#L500-L502: append the Zellij binding through a format-aware metadata writer.
  • bin/fm-backend.sh#L554-L556: use the same writer for the cmux binding.
  • tests/fm-teardown-endpoint-safety.test.sh#L215-L236: add a Zellij fixture with no final LF and assert a standalone binding.
  • tests/fm-teardown-endpoint-safety.test.sh#L264-L287: add the equivalent cmux regression case.
Proposed fix
+fm_backend_append_endpoint_task_id() {  # <meta-file> <task-id>
+  local meta=$1 id=$2 last_byte
+  last_byte=$(tail -c 1 "$meta") || return 1
+  [ -z "$last_byte" ] || printf '\n' >> "$meta" || return 1
+  printf 'endpoint_task_id=%s\n' "$id" >> "$meta"
+}
+
-          printf 'endpoint_task_id=%s\n' "$id" >> "$meta" || return 1
+          fm_backend_append_endpoint_task_id "$meta" "$id" || return 1
📍 Affects 2 files
  • bin/fm-backend.sh#L500-L502 (this comment)
  • bin/fm-backend.sh#L554-L556
  • tests/fm-teardown-endpoint-safety.test.sh#L215-L236
  • tests/fm-teardown-endpoint-safety.test.sh#L264-L287
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/fm-backend.sh` around lines 500 - 502, Preserve metadata record
boundaries when repairing bindings: in bin/fm-backend.sh lines 500-502, update
the Zellij repair path around fm_backend_zellij_pane_verifies_task to append
endpoint_task_id through a format-aware metadata writer that inserts a missing
final LF; apply the same writer to the cmux repair path at lines 554-556. In
tests/fm-teardown-endpoint-safety.test.sh lines 215-236 and 264-287, add
no-final-LF Zellij and cmux fixtures and assert each repaired binding is
standalone without corrupting the preceding field.

else
echo "REFUSED: legacy Zellij endpoint metadata for task $id lacks an exact task binding; pane verification failed or pane does not belong to this task; preserving task state." >&2
return 1
fi
fi
;;
orca)
# No legacy-metadata self-repair here, unlike herdr/zellij/cmux: Orca
# has no verified live-identity-check primitive to prove a recorded
# terminal/worktree still belongs to this exact task (see
# bin/backends/orca.sh's fm_backend_orca_json_get header - only
# worktree-id, worktree-path, terminal-handle, worktree-terminal-handle,
# and repo-id fields are verified real shapes; a task-owning label or
# title readback is not). Refusing unconditionally when the binding is
# absent is the correct, intended behavior here, not a gap: the brief's
# own rule is "refuse ONLY when it cannot be verified".
[ "$binding" = "$id" ] || {
echo "REFUSED: legacy Orca endpoint metadata for task $id lacks an exact task binding; preserving task state." >&2
return 1
Expand All @@ -522,10 +539,6 @@ fm_backend_validate_task_endpoint() { # <meta-file> <task-id>
window=$terminal
;;
cmux)
[ "$binding" = "$id" ] || {
echo "REFUSED: legacy cmux endpoint metadata for task $id lacks an exact task binding; preserving task state." >&2
return 1
}
workspace=$(fm_backend_meta_exact_value "$meta" cmux_workspace_id) || workspace=
surface=$(fm_backend_meta_exact_value "$meta" cmux_surface_id) || surface=
if [ -z "$workspace" ] || [ -z "$surface" ] || [ "$window" != "$workspace:$surface" ] \
Expand All @@ -534,6 +547,18 @@ fm_backend_validate_task_endpoint() { # <meta-file> <task-id>
echo "REFUSED: cmux endpoint metadata for task $id is malformed or inconsistent; preserving task state." >&2
return 1
fi
if [ "$binding" = "$id" ]; then
:
else
fm_backend_source cmux || return 1
if fm_backend_cmux_surface_verifies_task "$workspace" "$surface" "$id" 2>/dev/null; then
echo "cmux endpoint self-repair: appending endpoint_task_id=$id to metadata for legacy task $id" >&2
printf 'endpoint_task_id=%s\n' "$id" >> "$meta" || return 1
else
echo "REFUSED: legacy cmux endpoint metadata for task $id lacks an exact task binding; surface verification failed or surface does not belong to this task; preserving task state." >&2
return 1
fi
fi
;;
esac
# shellcheck disable=SC2034 # Output globals are consumed by sourcing callers.
Expand Down
18 changes: 17 additions & 1 deletion bin/fm-subagent-pretool-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ OBSERVE_ONLY_TOOLS='taskoutput taskstop taskget tasklist cronlist bashoutput kil
# widen by accident.
PLAN_ONLY_TOOLS='taskcreate taskupdate'

# Exact lowercase tool names that match the 'cron'/'schedul' stems but create or
# remove a DURABLE, PERSISTENT, INSPECTABLE schedule rather than the ephemeral,
# session-bound, fleet-invisible work the delegation guard targets. A cron
# ROUTINE outlives the session, is not started or owned by this session's own
# process, and is inspectable at any time through CronList (already exempt in
# OBSERVE_ONLY_TOOLS), so the guard's stated hazard - work that "dies with this
# session" and leaves no durable record anyone can see - simply does not fit it;
# the /schedule skill also depends on CronCreate. This is the create/delete
# counterpart to that observe-only CronList exemption. It is DELIBERATELY
# NARROWER than the 'cron schedul' stems: only these two exact names are
# released, so an unknown future cron- or schedule-noun tool (ScheduleCreate,
# CronSchedule, a background scheduler, etc.) is still caught by the stem match
# below and denied. Exact-name, never substring, so it cannot widen by accident,
# and tests/fm-subagent-pretool-check.test.sh locks it at exactly these two.
SCHEDULE_MGMT_TOOLS='croncreate crondelete'

TOOL=""
TOOL_SET=0
CLAUDE_MODE=0
Expand Down Expand Up @@ -152,7 +168,7 @@ case "$TOOL" in
mcp__*) exit 0 ;;
esac

for allowed in $OBSERVE_ONLY_TOOLS $PLAN_ONLY_TOOLS; do
for allowed in $OBSERVE_ONLY_TOOLS $PLAN_ONLY_TOOLS $SCHEDULE_MGMT_TOOLS; do
[ "$NORMALIZED" != "$allowed" ] || exit 0
done

Expand Down
14 changes: 14 additions & 0 deletions docs/cmux-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ Firstmate does not attempt to close the macOS window because cmux's socket canno
Real tests share the captain's running app rather than creating an isolated cmux session.
`tests/cmux-test-safety.sh` permits cleanup only for an exact currently listed `fm-test-` workspace and never enumerates and closes unrelated workspaces or relaunches the app.

## Endpoint metadata

```text
backend=cmux
window=<workspace-uuid>:<surface-uuid>
cmux_workspace_id=<workspace-uuid>
cmux_surface_id=<surface-uuid>
```

The UUID pair is the active endpoint authority within one app run.
Workspace UUIDs are not stable across an app relaunch, so recovery searches by the scoped title and then resolves the current surface id.
Comment on lines +113 to +123

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep one authoritative endpoint-metadata schema.

The new sections duplicate endpoint fields and identity rules already documented earlier in each file. Remove the repeated schema and rules, or replace them with a short link to the existing section. Keep the legacy self-repair paragraph.

  • docs/cmux-backend.md#L113-L123: remove the duplicate cmux schema and UUID rules.
  • docs/zellij-backend.md#L90-L102: remove the duplicate Zellij schema and pane-identity rules.
📍 Affects 2 files
  • docs/cmux-backend.md#L113-L123 (this comment)
  • docs/zellij-backend.md#L90-L102
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/cmux-backend.md` around lines 113 - 123, Remove the duplicated endpoint
metadata schema and UUID identity rules from docs/cmux-backend.md lines 113-123,
preserving the legacy self-repair paragraph and optionally linking to the
existing authoritative section. Apply the same cleanup to the duplicated Zellij
schema and pane-identity rules in docs/zellij-backend.md lines 90-102, with no
changes to the legacy self-repair paragraph.

Source: Coding guidelines


Legacy cmux metadata written before `endpoint_task_id=` existed self-repairs at teardown validation time instead of refusing outright: `fm_backend_validate_task_endpoint` verifies the live surface still exists within its recorded workspace and that the workspace still carries the task's label via `fm_backend_cmux_surface_verifies_task`, and only when both the surface membership and the workspace's title are confirmed to match (using `fm_backend_cmux_workspace_matches_label`'s scoped/bare-with-ambiguity rules), appends `endpoint_task_id=<id>` to the metadata file so the task tears down with no manual editing. When the live surface or workspace does not match - for example because the surface was recycled to a different workspace or the workspace was renamed to a different task's label - validation refuses without mutating the metadata, preserving the wrong-surface safety guarantee.

## Active limits

- cmux is experimental, macOS-only, GUI-first, and requires the app running.
Expand Down
Loading
Loading