Skip to content

fix(sync): --no-sync refuses a retained directory lease through an equivalent symlink path #2997

Open

Description

@coygeek

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.sh

Run 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 commit 4431387c9ae4a132972f8b8d97f9bc725bce1168; installed stock CLI reported 0.80.0.
  • Current default branch tested: main at ae86f38cee7fc505397bfaf266154314bd38cb42, freshly cloned and built using its declared toolchain; build reported dev.
  • Host: macOS 27.2, build 26B123.
  • Provider: local-container, Docker runtime 29.9.2, context desktop-linux; each run requested one CPU and 512m memory.
  • Guest: Ubuntu 26.04 LTS, Linux aarch64; image ubuntu@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea2648d2c0e09b.
  • No Git repository in the source; same external XDG_STATE_HOME and 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 -P preserves the intended no-sync result.

Activity

  1. added
    clawsweeper:intake:bugPreliminary submission classification; full source review pending.
    P2Normal priority bug or improvement with limited blast radius.
    clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.
    clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.
    clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.
    impact:ux-frictionUser-facing flow adds avoidable confusion or support burden without fully blocking progress.
    issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.
    no-staleExempts this issue from stale automation.
    and removed
    clawsweeper:intake:bugPreliminary submission classification; full source review pending.
    on Nov 1, 2026
  2. clawsweeper commented on Nov 1, 2026

    @clawsweeper
    Contributor

    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:

    • add P2: A supported retained-lease workflow fails before execution, with limited demonstrated scope and a physical-path workaround.
    • add impact:ux-friction: Equivalent directory paths trigger misleading foreign-owner and reclaim guidance.
    • add issue-rating: 🦞 diamond lobster: Current issue advisory state selects this label.
    • add clawsweeper:source-repro: Current issue advisory state selects this label.
    • add clawsweeper:queueable-fix: Current issue advisory state selects this label.
    • add 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:

    • go test -race -timeout=20m ./internal/cli -run 'Test.Directory.|TestClaimLeaseForRepoRejectsOtherRepoUnlessReclaimed'
    • go vet ./...
    • go test -race -timeout=20m ./...
    • Exercise retained directory-source lease creation from a physical root and no-sync reuse from its symlink alias with explicit logical PWD; verify both reuse paths succeed, another directory is refused, and stop removes the exact owned resources.

    What I checked:

    • Current default branch: GitHub main and the clean review checkout both resolve to ae86f38. (ae86f38cee7f)
    • Inconsistent source identity: findSyncRepo delegates directly to findRepo when synchronization is disabled; enabled directory mode instead canonicalizes the working directory. (internal/cli/sync_source.go:33, ae86f38cee7f)
    • Non-Git fallback preserves spelling: Failed Git discovery returns os.Getwd as the boundary root, and findRepo returns that root unchanged for a non-Git directory. (internal/cli/repo.go:399, ae86f38cee7f)
    • Observable refusal: The owner check rejects unequal nonempty root strings without reclaim; run supplies the discovered identity to lease resolution and claim publication. (internal/cli/claim.go:445, ae86f38cee7f)
    • Documented inactive synchronization: Directory mode selects the invocation directory; no-sync requires neither includes, enumeration, nor temporary Git metadata. (docs/features/sync.md:162, ae86f38cee7f)
    • Coverage gap and ownership control: TestRunDirectorySourceNoSyncDoesNotEnumerate checks inactive enumeration but stops at acquisition, without retained-lease alias reuse; TestClaimLeaseForRepoRejectsOtherRepoUnlessReclaimed covers genuinely different roots. (internal/cli/run_test.go:10570, ae86f38cee7f)
    How this review workflow works
    • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
    • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
    • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
    • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
    • Maintainers can also comment @clawsweeper review to request a fresh review only.
    • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge; those execution lanes are retired.
    • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

    Reviewed November 1, 2026, 1:21 AM ET / 05:21 UTC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal priority bug or improvement with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:ux-frictionUser-facing flow adds avoidable confusion or support burden without fully blocking progress.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.no-staleExempts this issue from stale automation.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions