Repository navigation
fix(sync): --no-sync refuses a retained directory lease through an equivalent symlink path #2997
Description
Summary
A retained lease created with sync.source: directory cannot be reused with run --id ... --no-sync when the shell reaches the same non-Git source directory through a symlink alias. On macOS, creating the lease from /private/tmp/... and reusing it from the equivalent /tmp/... path exits 2 with a claim-owner mismatch. Repeating the same command from the physical path succeeds without reclaiming the lease.
Steps to reproduce
Prerequisites: macOS where /tmp resolves to /private/tmp, installed Git and the tested Crabbox build, and a ready Docker-backed local-container provider. Use a fresh non-Git directory and keep state outside its source. The following expresses the executed fixture and command sequence; scratch names and lease IDs in the output below are sanitized.
task_dir=$(mktemp -d /tmp/crabbox-path.XXXXXX)
mkdir -p "$task_dir/source/scripts" "$task_dir/state"
cat > "$task_dir/source/crabbox.yaml" <<'YAML'
provider: local-container
sync:
source: directory
include: [scripts]
YAML
chmod 600 "$task_dir/source/crabbox.yaml"
cat > "$task_dir/source/scripts/check.sh" <<'SH'
#!/bin/sh
set -eu
printf 'PATH_PROBE_OK\n'
uname -sm
cat /etc/os-release
SH
physical=$(cd "$task_dir/source" && pwd -P)
logical="$task_dir/source"
export XDG_STATE_HOME="$(cd "$task_dir/state" && pwd -P)"
export CRABBOX_CONFIG="$physical/crabbox.yaml"
cd "$physical"
env PWD="$physical" crabbox run --provider local-container \
--local-container-cpus 1 --local-container-memory 512m \
--keep --lease-output "$task_dir/handle.json" -- sh scripts/check.sh
chmod 600 "$task_dir/handle.json"
lease=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["leaseId"])' "$task_dir/handle.json")
trap 'cd "$physical"; env PWD="$physical" crabbox stop --provider local-container --id "$lease"' EXIT
# Same directory through its logical alias; fails with exit 2.
cd "$logical"
env PWD="$logical" crabbox run --provider local-container --id "$lease" \
--no-sync --stop-after never -- sh scripts/check.sh
# Control: same lease, script, config and state; succeeds with exit 0.
cd "$physical"
env PWD="$physical" crabbox run --provider local-container --id "$lease" \
--no-sync --stop-after never -- sh scripts/check.shRun these commands in a shell without errexit, so the expected failing command does not skip the control. The explicit PWD values matter: changing a subprocess's cwd while leaving an inherited physical PWD may hide the failure. Both spellings were checked to name the same directory. No --reclaim was used.
Expected behavior
The same retained lease should remain reusable from either spelling of the same directory, with synchronization skipped and the uploaded script executed. The documented directory-source workflow selects the current directory, and the --no-sync contract disables enumeration and transfer; it does not describe a new owner merely because the current directory has an equivalent symlink spelling. A truly different source directory must still be refused unless adoption is explicit.
Actual behavior
Both tested builds produced this sequence once each:
create: exit 0
symlink-no-sync: exit 2
physical-no-sync: exit 0
stop: exit 0
provider inventory after stop: []
The symlink-path refusal was equivalent to the following, with the lease ID and unique scratch name replaced:
lease cbx_abcdef123456 is claimed by repo /private/tmp/crabbox-path-example/source; use --reclaim to claim it for /tmp/crabbox-path-example/source
The physical-path control printed:
PATH_PROBE_OK
Linux aarch64
Its run summary reported sync_skipped=true exit=0. This is an ordinary false refusal of the same owner's operation; no ownership bypass or security impact was observed. The physical-path workaround is cd "$(pwd -P)" before the follow-up run.
Runtime or environment
- Latest published release tested:
v0.80.0, source commit4431387c9ae4a132972f8b8d97f9bc725bce1168; installed stock CLI reported0.80.0. - Current default branch tested:
mainatae86f38cee7fc505397bfaf266154314bd38cb42, freshly cloned and built using its declared toolchain; build reporteddev. - Host: macOS
27.2, build26B123. - Provider:
local-container, Docker runtime29.9.2, contextdesktop-linux; each run requested one CPU and512mmemory. - Guest: Ubuntu
26.04 LTS, Linuxaarch64; imageubuntu@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea2648d2c0e09b. - No Git repository in the source; same external
XDG_STATE_HOMEand explicit fixture configuration for creation, both reuse attempts, and stop. One lease per build was created and stopped sequentially.
Evidence
- Expected source: directory-source and no-sync contract, together with run lease reuse and repository claims and that page's repository-claim section.
- Failure source: the executed creation/alias-reuse/physical-reuse sequence and sanitized output in Actual behavior, repeated on both tested builds.
- Evidence provenance: observed
- Local verification: reproduced
- Reproduction completeness: complete
The user-facing run path calls findSyncRepo(cfg, !*noSync). The directory-source helper canonicalizes the source root for sync-enabled runs but delegates no-sync discovery to findRepo. In a non-Git directory, repository discovery returns the working-directory spelling. The claim owner check compares the roots as strings. These are source breadcrumbs, not a prescribed implementation.
Each exact task container was independently absent from Docker after stop; provider inventory was empty, and its claim, per-lease key directory, and bootstrap directory were absent. No shared lease was reclaimed or stopped.
Restoration check
Repeat the fixture from the physical source path, then run the retained lease with --no-sync from the equivalent logical path while preserving logical PWD. Both reuse attempts must exit 0, print PATH_PROBE_OK, and report skipped synchronization without --reclaim. A control using a genuinely different directory must still receive the existing ownership refusal. Stop must still remove the exact owned container and local lease resources.
References
The completed directory-sync feature establishes the supported markerless-directory workflow; this report concerns subsequent no-sync reuse through an equivalent path. The merged static SSH collision-guidance change addresses two different repositories sharing a host, whereas this reproduction uses one directory and a local-container lease.
Triage assessment
- Impact level: P2
- Assessment status: supported
- Impact basis: Retained directory-mode execution fails before the command runs when an equivalent symlink spelling changes the local root string. The bounded sample reproduced this once on each tested build; reach beyond this macOS directory fixture was not tested.
- Workaround status: available
- Workaround basis: The same lease and uploaded script executed successfully from the physical directory path without reclaim or a transfer; changing into
pwd -Ppreserves the intended no-sync result.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
clawsweeper commentedon Nov 1, 2026
Codex review: this still needs some work.
Summary
This issue establishes a current source-level bug: directory-sync creation canonicalizes the source root, but no-sync reuse can retain an equivalent symlink spelling and fail the string-based ownership check.
Reproducibility: Current source establishes the unequal-root path through discovery and claim admission, matching the author's controlled macOS report; this read-only review did not execute the reproduction or tests.
Next step
Fix directory-source identity consistency and first establish a failing retained-lease regression with logical PWD, equivalent symlink paths, and a different-directory refusal control.
Review details
Best possible solution:
Use a consistent canonical identity for equivalent directory-source paths while retaining no-sync behavior and rejection of genuinely different owners.
Do we have a high-confidence way to reproduce the issue?
Current source establishes the unequal-root path through discovery and claim admission, matching the author's controlled macOS report; this read-only review did not execute the reproduction or tests.
Is this the best way to solve the issue?
A focused source-identity repair is appropriate; bypassing the owner check or recommending reclaim would obscure the defect and weaken the intended ownership contract.
AGENTS.md: found and applied where relevant.
Codex review notes: model internal, reasoning medium; reviewed against ae86f38cee7f.
Label changes
Label changes:
P2: A supported retained-lease workflow fails before execution, with limited demonstrated scope and a physical-path workaround.impact:ux-friction: Equivalent directory paths trigger misleading foreign-owner and reclaim guidance.issue-rating: 🦞 diamond lobster: Current issue advisory state selects this label.clawsweeper:source-repro: Current issue advisory state selects this label.clawsweeper:queueable-fix: Current issue advisory state selects this label.clawsweeper:fix-shape-clear: Current issue advisory state selects this label.Label justifications:
P2: A supported retained-lease workflow fails before execution, with limited demonstrated scope and a physical-path workaround.impact:ux-friction: Equivalent directory paths trigger misleading foreign-owner and reclaim guidance.Evidence reviewed
Acceptance criteria:
What I checked:
internal/cli/sync_source.go:33, ae86f38cee7f)internal/cli/repo.go:399, ae86f38cee7f)internal/cli/claim.go:445, ae86f38cee7f)docs/features/sync.md:162, ae86f38cee7f)internal/cli/run_test.go:10570, ae86f38cee7f)How this review workflow works
@clawsweeper re-reviewcomments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.@clawsweeper re-reviewor@clawsweeper re-runon an open PR or issue to request a fresh review only.@clawsweeper reviewto request a fresh review only.@clawsweeper explainto ask for more context, or@clawsweeper stopto stop active automation.Reviewed November 1, 2026, 1:21 AM ET / 05:21 UTC.