Skip to content

fix(archive): apply tar strip components once - #142

Open
Yigtwxx wants to merge 1 commit into
openclaw:mainfrom
Yigtwxx:fix/tar-strip-components-once
Open

fix(archive): apply tar strip components once#142
Yigtwxx wants to merge 1 commit into
openclaw:mainfrom
Yigtwxx:fix/tar-strip-components-once

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where consumers calling extractArchive() with stripComponents on a TAR whose members are ./-prefixed would have the whole extraction fail with a raw ENOENT, and — when the native binding is present — would get a different output tree than the JavaScript backend produces for the same bytes. The affected surface is archive extraction: path validation, entry filtering, the path-component limit, the collision tracker, and mode policy.

./-prefixed members are not exotic. tar cf pkg.tar . and tar czf pkg.tgz -C dir . both write them, and stripComponents is exactly what a consumer reaches for when unpacking such an archive.

The cause is that stripping happened twice, under two different rules. tar.x() received the strip count:

const extractor = tar.x({
  cwd: stagingDir,
  strip: Math.max(0, Math.floor(params.stripComponents ?? 0)),
  ...

and the filter callback separately computed its own stripped path for the accepted-entry plan:

(entry as { path: string }).path = normalizeArchiveEntryPath(info.path);   // "\" -> "/" only
const relPath = stripArchivePath(info.path, Math.max(0, Math.floor(params.stripComponents ?? 0)));

The two disagree on empty and . components. stripArchivePath() drops them before slicing (src/archive-entry.ts:80-93):

const parts = raw.split("/").filter((part) => part.length > 0 && part !== ".");
const stripped = strip === 0 ? parts.join("/") : parts.slice(strip).join("/");

node-tar does not. [CHECKPATH] splits raw and splices (node_modules/tar/dist/esm/unpack.js:261-278, tar 7.5.22):

const p = normalizeWindowsPath(entry.path);
const parts = p.split('/');
if (this.strip) {
  if (parts.length < this.strip) return false;
  ...
  parts.splice(0, this.strip);
  entry.path = parts.join('/');
}

normalizeArchiveEntryPath() only converts backslashes, so the leading ./ survived into node-tar's split. For ./pkg/hello.txt with stripComponents: 1:

  • node-tar wrote <staging>/pkg/hello.txt
  • fs-safe recorded hello.txt in acceptedEntries
  • the mode pass at src/archive.ts:444-451 then called fs.chmod(<staging>/hello.txt, ...)

Reproduced directly, before the fix:

Error: ENOENT: no such file or directory, chmod '...\fs-safe-archive-C0pJL2\hello.txt'
 ❯ Object.run src/archive.ts:450:15
 ❯ withStagedArchiveDestination src/archive-staging.ts:335:12

Two consequences beyond the crash are worth stating separately:

  1. Policy ran against a path other than the one written. entryFilter sees the entry, and assertArchiveEntryPathComponentsWithinLimit() and createArchiveOutputPathTracker() both key on the fs-safe-stripped path (src/archive-tar.ts:82-89), while node-tar wrote a longer one. The depth limit was therefore measured one component short, and two entries could be judged non-colliding under one spelling and written under another. Containment was never at risk — extraction goes into a private staging tree that is merged back through the normal boundary checks — but the policy decisions did not describe the bytes on disk.

  2. The backends disagreed. src/archive-native.ts:107 feeds stripArchivePath() into the Rust plan, so the native backend placed the file at hello.txt. Same archive, same options, two different trees depending on whether a prebuilt binding was loaded.

Why This Change Was Made

fs-safe now owns stripping outright. The filter callback assigns the already-stripped path back to the entry, and tar.x() is given strip: 0:

const relPath = stripArchivePath(info.path, strip);
if (!relPath) {
  return false;
}
// Hand node-tar the exact path this entry was validated,
// limited and collision-checked under, and that the mode
// pass below resolves after extraction.
(entry as { path: string }).path = relPath;

This works because filter is a Parser option (node_modules/tar/dist/esm/parse.js:233) and runs before Unpack[ONENTRY]Unpack[CHECKPATH]. Unpack extends Parser, so the mutation the filter makes is exactly what [CHECKPATH] and entry.absolute see. stripArchivePath() becomes the single authoritative strip rule for both backends, which is what test/extracted-helpers.test.ts:125-126 already pins.

Behavior that was checked and is deliberately unchanged:

  • Link entries. createTarEntryPreflightChecker() hard-rejects SymbolicLink, Link, and device/FIFO/socket types (src/archive-tar.ts:103-108) before Unpack ever sees the entry, so node-tar's entry.type === 'Link' linkpath-rewrite branch was already unreachable here. Dropping strip removes no live behavior.
  • Entries that strip away to nothing (the ./ root member, or a path with fewer real components than stripComponents) are still skipped rather than crashing. The preflight checker already returns false for exactly this case (src/archive-tar.ts:79-82), so the if (!relPath) return false guard in the filter is defensive narrowing, not a new rule.
  • maxDepth. node-tar now measures depth on the stripped path, which has the same or fewer components than before, so it can only become less likely to fire. fs-safe's own 256-component limit remains the binding constraint.
  • [STRIPABSOLUTEPATH]. Unreachable either way: relPath has already passed validateArchiveEntryPath(), which rejects absolute paths, //, .., drive letters, NUL, and (on win32) ADS spellings. Under strict: true this makes the TAR_ENTRY_INFO warning strictly less reachable, not more.

strip: 0 is passed explicitly rather than omitted because TarExtractOptions in src/archive-tar-runtime.ts:23 declares strip as required.

Non-goals: ZIP extraction already resolves its own output path and is untouched. stripArchivePath() itself is unchanged — the bug was in having a second, different implementation, not in this one.

User Impact

Extracting a ./-prefixed TAR with stripComponents now succeeds and places files where stripComponents says, instead of failing with ENOENT during the mode pass. The JavaScript and native backends now produce the same tree for the same archive, and the entry filter, path-component limit, and collision tracker now apply to the path that is actually written.

For archives without . or empty path components — including everything tar.c() produces, and the package/-prefixed layout npm and GitHub tarballs use — output paths are byte-identical to before, because the two strip rules already agreed there. That is also why no existing test needed updating.

One behavior worth calling out for anyone who was working around the old shape: an entry whose stripped path is empty is skipped rather than extracted at its unstripped location. That was already true — the preflight checker filtered it — so this is a restatement, not a change.

Evidence

One regression test in test/native-archive-equivalence.test.ts, added next to the two existing stripComponents tests inside describe.each(archiveBackends) so it asserts backend parity. That suite falls back to ["javascript"] when no binding is built and is re-run with a real binding by the CI native job, so the parity claim is enforced in CI even though I have no Rust toolchain locally.

The fixture is the exact shape tar cf pkg.tar . writes:

tarFixture([
  { path: "./pkg/", type: "5" },
  { path: "./pkg/hello.txt", body: "hi", mode: 0o755 },
])

Before the fix:

 ❯ test/native-archive-equivalence.test.ts (22 tests | 1 failed | 3 skipped)
     × strips leading dot components once and applies modes to the extracted path

Error: ENOENT: no such file or directory, chmod '...\fs-safe-archive-C0pJL2\hello.txt'
 ❯ Object.run src/archive.ts:450:15

After:

 Test Files  1 passed (1)
      Tests  19 passed | 3 skipped (22)

The mode assertion is the load-bearing one. It only passes if the mode pass resolved a path that exists, which is the same thing as saying acceptedEntries and the extracted tree agree — the exact invariant that was broken.

Every existing stripComponents and stripArchivePath site was checked and none needed changing:

site why it is unaffected
test/api-coverage.test.ts:499 archive built with tar.c(...), so members are package/... with no . component; both rules already agreed
test/archive.test.ts:132 ZIP, a different code path
test/coverage-gaps.test.ts:161 unit test of the preflight checker, which is unchanged
test/native-archive-equivalence.test.ts:208 entry-count limit throws before stripping
test/native-archive-equivalence.test.ts:234 collision tracker throws before any entry.path assignment
test/extracted-helpers.test.ts:125-126 stripArchivePath() unit tests; the helper is unchanged
test/property-fuzz-stress.test.ts:152 same helper

No test in the repository extracted a ./-prefixed or //-containing TAR with stripComponents > 0. The two ./-prefixed fixtures that exist (test/native-archive-equivalence.test.ts:88, test/archive-read-boundaries.test.ts:294) go through readArchiveEntry() rather than extractArchive(). That gap is why this stayed latent.

Archive suites after the fix:

$ pnpm vitest run test/archive-policy.test.ts test/archive.test.ts \
    test/archive-read-boundaries.test.ts test/native-archive-equivalence.test.ts \
    test/archive-portable-collisions.test.ts test/archive-staging.test.ts \
    test/archive-adjacent-errors.test.ts

 Test Files  7 passed (7)

Native parity, confirmed by CI on this head

The regression sits inside describe.each(archiveBackends), and .github/workflows/ci.yml runs that file against a real binding after pnpm native:build:

- name: Test native security and path equivalence
  run: pnpm test test/native-integration.test.ts test/native-write-containment.test.ts
       test/native-archive-equivalence.test.ts test/native-publish-equivalence.test.ts
       test/private-directory.test.ts

Green on 326e815 across every native target:

Native check (windows-latest)   pass
Native check (macos-15)         pass
Native check (ubuntu-latest)    pass
Native check (linux-x64-musl)   pass

So the same fixture asserted readdir === ["hello.txt"] under the shipped binding as well as under the JavaScript fallback. The native path never had the defect — src/archive-native.ts:107 already fed stripArchivePath() into the Rust plan — and this PR changes no Rust. The parity test exists so the two cannot drift apart again, which is the failure mode docs/archive.md:153 warns about.

Full pnpm check on Windows 11 / Node 22.20.0 / pnpm 10.34.5:

lint:file-size    ok       (src/archive.ts 481 -> 483 lines, budget 500)
lint:fs-boundary  ok
build             ok
docs:check        documentation examples match the built package
test              Test Files 1 failed | 94 passed | 9 skipped (104)
                  Tests      1 failed | 814 passed | 292 skipped (1107)

No export moves, so test/public-api.json and check-pack are untouched. The docs/archive.md addition is prose, which scripts/check-doc-examples.mjs does not execute; it states the component-counting rule and that the JavaScript TAR path hands node-tar the already-stripped path.

The one red test is pre-existing and unrelated

file-lock-sync-failure.test.ts > fails closed when a synchronous lock parent cannot match the Root canonical path fails on this machine on a clean origin/main checkout, before any change from this PR. It is environment-dependent: the test uses useTempDirs(), which returns the raw mkdtemp() path without a realpath, and then asserts that a lock path whose spelling cannot match lockRoot.rootReal is rejected. That mismatch only exists when the ambient temp directory is not already canonical — true on the hosted Windows runner (C:\Users\RUNNER~1\...), false on a Windows account whose profile directory needs no 8.3 alias. Nothing in this diff touches locking.

  • Tests added or updated when behavior changed
  • Security and compatibility impact considered
  • CHANGELOG.md updated when release-relevant
  • No credentials, private paths, private hosts, or sensitive contents included

JavaScript TAR extraction stripped leading path components twice, under two
different rules. tar.x() received strip: N, and the filter separately computed
stripArchivePath(info.path, N) for the accepted-entry plan.

stripArchivePath() drops empty and "." components before slicing; node-tar's
CHECKPATH does a raw split and splices N off whatever it finds. For an entry
named ./pkg/hello.txt with stripComponents: 1, node-tar wrote pkg/hello.txt
while fs-safe recorded hello.txt, so the post-extraction mode pass failed with
ENOENT on a path that was never created, and the entry filter, path-component
limit, and collision tracker all ran against a path other than the one on disk.
The native backend uses stripArchivePath(), so the two backends also produced
different trees for the same archive.

The filter now assigns the already-stripped path back to the entry, and tar.x()
is given strip: 0. node-tar writes exactly the path fs-safe validated, limited,
collision-checked, and chmods. Entries that strip away to nothing are still
skipped -- the preflight checker already rejected them for the same reason.
@Yigtwxx
Yigtwxx requested a review from a team as a code owner August 23, 2026 16:26
@clawsweeper

clawsweeper Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Aug 23, 2026
@clawsweeper

clawsweeper Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed August 23, 2026, 12:55 PM ET / 16:55 UTC.

ClawSweeper review

What this changes

The PR makes JavaScript TAR extraction apply stripComponents once so ./-prefixed entries are written at the same validated path used by the native backend.

Regression provenance

Possible regression — probable (reviewed change; failure trace). No predecessor PR is attributed.

Merge readiness

⚠️ Ready for maintainer review - 3 items remain

This PR remains necessary: current main still gives node-tar a separate strip count after fs-safe has planned the stripped path, while the branch makes that path authoritative and adds focused parity coverage. No actionable correctness or security regression was found.

Priority: P2
Reviewed head: 326e8159a7482033745567dd62d9f1dfb46fca3c

Review scores

Measure Result What it means
Overall readiness 🦞 diamond lobster (5/6) A focused fix with strong direct failure evidence, backend-parity coverage, and no identified patch defect.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (logs): The contributor supplied before-and-after extraction output plus recorded native-backend CI parity across four platforms; redact private paths if any future logs are added.
Patch quality 🦞 diamond lobster (5/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (logs): The contributor supplied before-and-after extraction output plus recorded native-backend CI parity across four platforms; redact private paths if any future logs are added.
Evidence reviewed 6 items Current-main mismatch: Current main gives node-tar the configured strip count while separately computing fs-safe’s stripped accepted-entry path, leaving two stripping rules in the JavaScript extraction path.
Authoritative branch path: The branch validates through the existing preflight checker, computes one stripped relative path, assigns that exact path to node-tar, and records it for the mode pass.
Focused regression coverage: The new backend-parameterized test extracts ./pkg/hello.txt with one stripped component and asserts both resulting location and mode.
Findings None None.
Security None None.

Live Verification

Command: pnpm test test/native-archive-equivalence.test.ts

Result: FAIL (partial) — step 2 expect_output strips leading dot components once and applies modes to the extracted path: expected terminal output was not visible within 30 seconds: "strips leading dot components once and applies modes to the extracted path"

pnpm test test/native-archive-equivalence.test.ts
runner@runnervm76f27:/tmp/clawsweeper-live-proof-142-nZPYnr/target$ pnpm test test/native-archive-equivalence.test.ts

› @openclaw/fs-safe@0.5.6 test /tmp/clawsweeper-live-proof-142-nZPYnr/target
› vitest run test/native-archive-equivalence.test.ts


 RUN  v4.1.10 /tmp/clawsweeper-live-proof-142-nZPYnr/target

pnpm test test/native-archive-equivalence.test.ts
 ✓ test/native-archive-equivalence.test.ts (22 tests | 3 skipped) 216ms

 Test Files  1 passed (1)
      Tests  19 passed | 3 skipped (22)
   Start at  16:56:21
   Duration  805ms (transform 349ms, setup 0ms, import 463ms, tests 216ms, environment 0ms)

runner@runnervm76f27:/tmp/clawsweeper-live-proof-142-nZPYnr/target$ pnpm test test/native-archive-equivalence.test.ts

› @openclaw/fs-safe@0.5.6 test /tmp/clawsweeper-live-proof-142-nZPYnr/target
› vitest run test/native-archive-equivalence.test.ts


 RUN  v4.1.10 /tmp/clawsweeper-live-proof-142-nZPYnr/target

 ✓ test/native-archive-equivalence.test.ts (22 tests | 3 skipped) 200ms

 Test Files  1 passed (1)
      Tests  19 passed | 3 skipped (22)
   Start at  16:56:22
   Duration  777ms (transform 335ms, setup 0ms, import 450ms, tests 200ms, environment 0ms)

runner@runnervm76f27:/tmp/clawsweeper-live-proof-142-nZPYnr/target$


















Assertions:

  • FAIL expect_output: strips leading dot components once and applies modes to the extracted path

How this fits together

Archive extraction validates untrusted TAR paths, plans safe staged outputs, writes the files, applies modes, and then merges the staged tree into the destination. JavaScript and native extraction must apply the same path, collision, depth, and mode policies.

flowchart LR
  A[Untrusted TAR archive] --> B[Validate entry path]
  B --> C[Strip real path components]
  C --> D[Check depth and collisions]
  D --> E[Write into private staging]
  E --> F[Apply entry modes]
  F --> G[Safely merge destination]
Loading

Before merge

  • Resolve merge risk (P1) - This intentionally corrects the output tree for TAR names containing leading . or empty components; consumers relying on the prior JavaScript-only shape will observe the documented native-compatible result.
  • Resolve merge risk (P1) - The patch changes a security-sensitive extraction boundary, although validation, collision checks, staging, and post-write mode application all now use the same relative path.
  • Complete next step (P2) - No mechanical repair is needed: the reviewed patch has no actionable findings and the remaining action is normal maintainer merge review.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Patch scope 4 files affected; 58 added, 23 removed The change stays focused on one extraction invariant, its regression test, and matching documentation/release notes.
Code versus tests runtime +3 net, docs +3, tests +29 Small production growth is paired with focused backend-parity and mode coverage.

Merge-risk options

Maintainer options:

  1. Land the documented path correction (recommended)
    Accept the corrected output location for leading-dot and empty TAR components, backed by the JavaScript/native parity regression and green native CI.

Technical review

Best possible solution:

Land the single-authoritative-path approach with the included parity regression so both backends keep applying archive policy to the path actually written.

Do we have a high-confidence way to reproduce the issue?

Yes—source reproducible with a TAR entry named ./pkg/hello.txt and stripComponents: 1: current main plans hello.txt but asks node-tar to strip the raw name independently, while the branch’s regression covers the corrected path and mode.

Is this the best way to solve the issue?

Yes—the patch removes the duplicate strip implementation rather than changing validation or containment policy, making one fs-safe-normalized path govern both backends and the post-extraction mode pass.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 650e0162c20b.

Labels

Label justifications:

  • P2: This is a bounded correctness repair on a public archive-extraction surface, without evidence of active data loss or a bypass.
  • merge-risk: 🚨 compatibility: Existing JavaScript users with leading-dot or empty TAR components will receive the corrected native-compatible output tree.
  • merge-risk: 🚨 security-boundary: The patch changes how validated untrusted archive names are handed to the extraction engine, a filesystem boundary.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (logs): The contributor supplied before-and-after extraction output plus recorded native-backend CI parity across four platforms; redact private paths if any future logs are added.
  • proof: sufficient: Contributor real behavior proof is sufficient. The contributor supplied before-and-after extraction output plus recorded native-backend CI parity across four platforms; redact private paths if any future logs are added.

Evidence

What I checked:

  • Current-main mismatch: Current main gives node-tar the configured strip count while separately computing fs-safe’s stripped accepted-entry path, leaving two stripping rules in the JavaScript extraction path. (src/archive.ts:384, 650e0162c20b)
  • Authoritative branch path: The branch validates through the existing preflight checker, computes one stripped relative path, assigns that exact path to node-tar, and records it for the mode pass. (src/archive.ts:396, 326e8159a748)
  • Focused regression coverage: The new backend-parameterized test extracts ./pkg/hello.txt with one stripped component and asserts both resulting location and mode. (test/native-archive-equivalence.test.ts:240, 326e8159a748)
  • Native contract alignment: The current native path already uses stripArchivePath() before its output-path, limit, and collision checks, so the change aligns JavaScript with the established native rule. (src/archive-native.ts:107, 650e0162c20b)
  • History and ownership provenance: Current archive preflight lines trace to the v0.5.6 tree; earlier archive-safety work is also attributed to Peter Steinberger in commit ec5519f. (src/archive-tar.ts:61, 01ef06f04e49)
  • After-fix real behavior proof: The PR body and follow-up comment provide before/after terminal output and report green native parity on Windows, macOS, Ubuntu, and musl CI for this head. (test/native-archive-equivalence.test.ts:240, 326e8159a748)

Likely related people:

  • Peter Steinberger: Current archive preflight provenance and the earlier archive-safety commit both identify Peter in this subsystem’s history. (role: recent archive-safety contributor; confidence: medium; commits: 01ef06f04e49, ec5519f8eecf; files: src/archive-tar.ts, src/archive-native.ts, src/archive.ts)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • 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.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (1 earlier review cycle)
  • reviewed 2026-08-23T16:29:17.824Z sha 326e815 :: needs maintainer review before merge. :: none

@Yigtwxx

Yigtwxx commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Native parity is already covered on this head, so I want to point at it rather than leave it as an open item.

The regression went into test/native-archive-equivalence.test.ts inside describe.each(archiveBackends), and .github/workflows/ci.yml runs that file with a real binding after pnpm native:build:

- name: Test native security and path equivalence
  run: pnpm test test/native-integration.test.ts test/native-write-containment.test.ts
       test/native-archive-equivalence.test.ts test/native-publish-equivalence.test.ts
       test/private-directory.test.ts

That job is green on 326e815 across all four native targets:

Native check (windows-latest)   pass   .../job/97224205072
Native check (macos-15)         pass   .../job/97224205043
Native check (ubuntu-latest)    pass   .../job/97224205082
Native check (linux-x64-musl)   pass   .../job/97224205053

So the same fixture — ./pkg/ plus ./pkg/hello.txt extracted with stripComponents: 1 — asserted readdir === ["hello.txt"] under the shipped binding as well as under the JavaScript fallback. That is the parity claim, run rather than argued.

Worth stating explicitly why it holds: the native path never had the defect. src/archive-native.ts:107 already fed stripArchivePath() into the Rust plan, so hello.txt is where it always landed. This PR does not change Rust at all; it moves the JavaScript backend onto the same rule. The parity test is there so the two cannot drift apart again, which is the failure mode docs/archive.md:153 warns about.

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

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant