CLI and read-only scanning
The current sweepx binary is the sole executable entry point. It exposes scan, junk, explain, status, cancel, cache, cleaner, trash, and capabilities; scan --tui enters after root admission and scans progressively in the background.
CAUTION
trash is a development preview: it only moves an item to the operating-system Trash, confirms by default, and revalidates before submission. It never falls back to Permanent deletion. plan, approve, execute, Permanent, and --dangerously-delete remain roadmap proposals.
Build and inspect capabilities
Run from the repository root:
cargo build -p sweepx-cli
cargo run -p sweepx-cli -- --locale en-US capabilitiesGlobal options:
| Option | Meaning |
|---|---|
| `--format human | json |
| `--locale zh-CN | en-US` |
| `--unit auto | b |
| `--sort size | path` |
--state-dir ABSOLUTE_DIR | Select the SQLite journal directory on Linux or legacy snapshot directory on macOS; Windows defaults to %LOCALAPPDATA%\sweepx\state; the directory must be reachable only by the current user or the command fails closed |
--elevate | Windows-only in effect, off by default. If the current process is not elevated, request one UAC consent and relaunch itself elevated before doing anything else; the parent then returns the child's exit code unchanged and relays the child's standard output to its own, so pipes and redirection behave exactly as they do unelevated. An elevated process cannot inherit the parent's console -- runas creates a new one that closes when the child exits -- so the child writes to a file the parent generated inside its own private temporary directory, which the parent reads back and forwards. That path is always generated by the parent; an inherited one is stripped. An already-elevated process never relaunches; a declined prompt or an unsupported platform continues at the current privilege level without changing scan results. The flag is not forwarded to the relaunched child, so a second elevation is structurally impossible |
scan --no-state | Skip Linux journal or macOS legacy-snapshot writes when later status/operation state is unnecessary or the state filesystem does not support the journal; conflicts with --state-dir |
Locale resolution considers the explicit override, locale environment, and system locale; an unrecognized result falls back to en-US. Machine keys and values are not translated.
P4a.2 qualification records are not a new command
The protocol can now express one exact capability/platform tuple and its evidence as a typed, validated record. Mutation does not use a broad delete flag: it is split into trash.local.file, trash.local.directory, permanent.local.file, permanent.local.directory, and permanent.local.link. The current host's two Trash cells are reported as a degraded preview; other platforms and every Permanent cell remain disabled.
These records remain a fail-closed qualification-registry substrate; a degraded preview is not qualified. fixture_conformance_only, fake, stale, incomplete, placeholder, or mismatched evidence can never qualify mutation. A cell could become qualified later only if current real_os_qualification evidence completely matches its exact tuple. There is still no plan/approval UI or Permanent adapter.
Install
A release produces archives plus one SHA256SUMS for Linux x86_64/aarch64, macOS Intel/Apple Silicon, and Windows x86_64. The installers verify the checksum and require the archive to contain only a root-level sweepx or sweepx.exe.
curl --proto '=https' --tlsv1.2 -fsSL \
https://raw.githubusercontent.com/lejunyang/sweepx/main/install.sh | shirm https://raw.githubusercontent.com/lejunyang/sweepx/main/install.ps1 | iexRelease infrastructure does not mean a stable release already exists. Check GitHub Releases and sweepx capabilities before installing.
Normal pushes and pull requests run Rust, schema, site, installer, and native-CLI CI. GitHub Pages deploys independently when main changes. Binary and crates.io publication run only when the HEAD commit message contains the literal [publish] marker. GitHub Release and Pages need no extra token; crates.io requires CARGO_REGISTRY_TOKEN in a protected crates-io environment.
Development-grade read-only scans on all three platforms
cargo run -p sweepx-cli -- scan /absolute/path/to/root
# Explicitly skip writes when later operation state is unnecessary
cargo run -p sweepx-cli -- scan --no-state /absolute/path/to/root- With no roots,
scanselects the current platform filesystem root; you may instead provide relative paths,~, or one or more absolute roots. - The default writes a bounded 40-row file table directly to the terminal; no JSON file is required.
- The scan runs synchronously, is metadata-only and no-follow, and reports mount/link/resource boundaries and errors.
- The current Linux capability is
degraded, not release qualification. - The macOS backend now exposes a handle-bound degraded scanner through the same
scan/scan --tuipath; that is not release qualification. - The Windows backend now provides a handle-relative degraded read-only scanner through
scan/scan --tui; that is not release qualification. - Linux may explicitly select a SQLite journal directory; macOS may select a legacy snapshot directory; Windows may select a durable state directory, which must be reachable only by the current user.
scan --format ndjsoncurrently returns unsupported before scanning. Linux has a bounded SQLite journal, one-transaction complete-stream/terminal persistence, journal-first status, and degraded completed replay throughsweepx --format ndjson status --operation-id ID --watch [--after SXCUR1]: it performs one same-snapshot full validation, then reads pages of at most 1024 events from a completed, persisted stream; an unknown but syntactically valid cursor yieldsstream.reset_required, while malformed cursor usage remains a usage error. Because events are still constructed after the scan, that replay is not live streaming, does not wait for new events, does not create a background operation, and does not support cancel, soscan --format ndjsonremains disabled.
Request machine output explicitly for scripts and integrations:
sweepx --format json scan /absolute/path/to/root > scan.json
# Currently returns unsupported; it does not scan
sweepx --format ndjson scan /absolute/path/to/rootStatus snapshots and cancellation
On Linux or macOS, after reading operationId from scan output, the corresponding terminal snapshot can be queried:
cargo run -p sweepx-cli -- \
--format json \
--state-dir /absolute/path/to/sweepx-state \
status --operation-id <OPERATION_ID>
cargo run -p sweepx-cli -- \
--format ndjson \
--state-dir /absolute/path/to/sweepx-state \
status --operation-id <OPERATION_ID> --watch [--after SXCUR1_CURSOR]
cargo run -p sweepx-cli -- \
--format json \
--state-dir /absolute/path/to/sweepx-state \
cancel --operation-id <OPERATION_ID>status reads persisted terminal state journal-first on Linux and supports degraded completed replay through sweepx --format ndjson status --operation-id <OPERATION_ID> --watch [--after SXCUR1]: it covers only completed, persisted streams, performs one same-snapshot full validation, then returns pages of at most 1024 events; an unknown but syntactically valid cursor yields stream.reset_required, while malformed cursor usage remains a usage error. It does not wait for new events, does not create a background operation, and does not support cancel, so it is not live progress. macOS reads from the legacy snapshot and still has no replay/watch surface. On Windows, state_dir defaults to %LOCALAPPDATA%\sweepx\state and durable snapshots are written there; a state directory reachable by other users fails closed. There is no live in-process registry, so output reports canCancel: false and cancellation remains disabled. The cancel command exists to distinguish not_found, already_terminal, and unsupported honestly, not to pretend it can interrupt the synchronous scan.
Read-only preview-cache diagnostics
cargo run -p sweepx-cli -- \
--format json \
--state-dir /absolute/path/to/sweepx-state \
cache status- Linux, macOS and Windows all support
cache status. - It supports
humanandjsononly;--format ndjsonfails with a usage error before any state/cache directory is created or read. - If default or explicit state/cache is missing, the result returns
disposition=absentwith exit 0 and does not createstate_dir,preview-cache/,current.json, or any generation/quarantine directory. - Inspection is strictly bounded to
preview-cache/current.json, the pointer-selected current generation file, and the flatgenerations/andquarantine/directories. - The output kind is
cache.status.result, reportingexists,currentGeneration,generationCount,quarantineCount,approxBytes,approxBytesComplete,storedSchema,currentHealth,schemaHealth, and typedwarnings[]/errors[]. - The command does not trigger a scan, repair, quarantine, rebuild, or reveal cached entries, display paths, preview contents, or live filesystem facts.
availablemeans only that bounded cache structure and validation are readable; any warning, error, or quarantine presence degrades the result to exit 4.
Explain from scan JSON
cargo run -p sweepx-cli -- \
--format json \
explain \
--scan-json /absolute/path/to/scan.json \
--candidate-id <OPTIONAL_CANDIDATE_ID> \
--max-input-bytes 8388608Input must be an absolute path, conform to the scan.result contract, and stay within the byte limit. On import, Core:
- changes provenance to stale preview;
- marks coverage incomplete/not revalidated;
- produces an explanation while forcing the candidate non-executable/report-only.
The result is useful for understanding, not for planning or execution.
Cleaner metadata
cargo run -p sweepx-cli -- --format json cleaner list
cargo run -p sweepx-cli -- --format json cleaner show org.sweepx.cargo-targetlist reports packages and compatibility. show exposes full manifest/rule metadata only when the Core version range matches; incompatibility uses a dedicated fail-closed exit. Neither command performs a file action described by a rule. See Cleaner concepts.
A unified junk-discovery entry point is available:
sweepx junk ~/Projects
sweepx --format json junk .
sweepx --format json junk --systemExplicit roots continue to discover clearly rebuildable project artifacts: Rust target, Node node_modules, Python __pycache__/.pytest_cache/.mypy_cache/.ruff_cache, and common dist/build/out/.next/.turbo outputs. With no explicit root, --system reports individual application-cache children below an absolute XDG_CACHE_HOME (or ~/.cache) on Linux and ~/Library/Caches on macOS; on Windows it admits only depth-2 LocalCache / TempState directories below %LOCALAPPDATA%/Packages. --system conflicts with explicit roots. Every result remains report-only and includes the rule ID, risk, source-review date, first-party references, and reclaimable estimate; Linux /tmp//var/tmp, Windows system cleanup, and package-manager/container shared stores are not admitted by directory-name matching.
How a candidate's size is reported
reclaimable prefers the filesystem's allocated size. When the platform will not claim allocation, the apparent logical size is reported instead and sizeIsLogical is true.
This matters on Windows, where the adapter deliberately refuses to claim allocation: FILE_STANDARD_INFO describes only the unnamed $DATA stream, so an exact figure would be a guess wherever alternate streams, sparse ranges or compression are involved. That refusal is correct, but taken literally it left every candidate sizeless — measured 2026-09-05, 30 of 30, including 1.8 GB of browser caches. A tool that cannot say how large anything is has not answered the question.
The two quantities are not interchangeable, so the substitution is always visible rather than silent. An allocation that is merely a lower bound does not win by being the nominally correct field: an exactly known logical size carries more information. When neither is exact, the allocation-derived evidence is kept, because its reason code explains why the size is missing.
Browser render caches
--system reports the rebuildable caches of every Chromium-family installation it finds: the HTTP cache, the compiled JavaScript and WebAssembly cache, and the GPU and shader caches. Discovery enumerates profiles from disk rather than assuming Default, and also covers the shader caches that sit beside the profiles rather than inside one.
Each of the three backends has a different layout, so each rule is guarded by its own marker — Cache_Data, js, and data_1 respectively. An index file cannot be used as a common marker: measured 2026-09-05, two of the three carry none at their root.
What is deliberately not matched: Service Worker CacheStorage, IndexedDB, Local Storage, cookies, and extension state. CacheStorage is named cache but holds PWA offline state, not responses the network can fetch again.
Measured on Windows 2026-09-05 across Edge, Edge Dev, and Chrome: 1.8 GB total, the largest single directory being 611.7 MB of Edge Dev code cache. Assuming a single browser installation would have missed it.
Sizes for a blockfile-backed shader cache include scaffolding — data_0 through data_3 and index are written even when the cache is empty, so an empty one still occupies about 0.5 MB.
Tool caches: every copy, not just the live one
Rules for npm, pnpm and pip do not trust a single location. Discovery enumerates the path the tool reports, the tool's environment override, and the documented platform defaults, then admits a candidate only if its own contents match that cache's layout — for a pnpm store, a files/ directory holding exactly 256 two-hex-digit shards.
Asking the tool answers which copy is live, and the live copy is the one that must not be reclaimed. The abandoned copy is the junk, and it is never the one the resolver names. Measured on 2026-09-05: the pnpm store at the documented default held 146.8 MB last written 2024-10-26, while the store actually in use held 127.5 MB on another volume — a resolver-only rule misses the larger, inert copy entirely.
Two markers are reported. Neither deletes, pre-selects, or reorders anything; classification stays report-only.
| Field | Values | Meaning |
|---|---|---|
activity | live | The tool reports this path. Do not reclaim it. |
stale | A verified cache of this tool that the tool is not using. | |
unknown | The tool could not be asked, so nothing is claimed. Absence of an answer is not evidence of abandonment. | |
staleFormats | e.g. ["http"] | Superseded format directories inside a root whose current format is also present. |
unknown exists because of a measured failure mode: on Windows npm ships as a .cmd/.ps1 shim and a direct process spawn does not apply PATHEXT, so the resolver returned nothing and the live cache was briefly labelled stale. Directory identity is likewise resolved through the filesystem rather than by comparing path strings, because case sensitivity is a property of the host and volume; comparing spellings reported one pip cache three times.
staleFormats is reported only when a current generation is present alongside the old one. Otherwise the tool is simply an older version whose only format is the one on disk, and calling it superseded would be wrong. Measured on 2026-09-05: pip's cache held the legacy http format at 73.1 MB last written 2023-12-09 beside the current http-v2 at 0 MB — 99.9% of the bytes sat in a format nothing writes to any more, inside a root that is otherwise live.
File-manager-style TUI and Trash preview
cargo run -p sweepx-cli -- --locale en-US \
scan --tui /absolute/path/to/root [/another/absolute/root]
cargo run -p sweepx-cli -- trash /absolute/path/to/itemThe TUI consumes the typed result of this live scan without an intermediate JSON file. It auto-enters a single root; multiple roots first appear in a virtual-root view. Use Enter / Right / l to enter a directory, Esc / Backspace / Left / h to go back, arrows or j/k to move, d / Delete to select an item for Trash, and q or Ctrl-C to quit. Trash exits the full-screen view, asks for confirmation, and revalidates the live scan identity; symlinks and reparse points cannot be mutated.
--tui requires terminal stdin and stdout and cannot be combined with --format json|ndjson. The TUI enters after root admission and auto-opens a single root. Direct children appear first; while the background scan runs, recursive directory totals are merged and resorted as explicit lower bounds (>=) at roughly 120 ms intervals. The final result then converges to exact or explicitly incomplete evidence, without retaining descendants as list rows. A one-slot progress channel drops superseded intermediate snapshots instead of applying terminal backpressure. Detail rescans are single-flight; 30 seconds is a no-progress deadline renewed by valid updates, and navigation or quit does not wait for a non-cooperative worker.
See MangoDisk adoption decisions for acceleration findings, rule provenance, the GPL boundary, and the Linux policy.
Windows scan acceleration and privilege
Windows has an accelerated scan path built on native NTFS metadata. Every scan root is qualified read-only before traversal, and a refusal never affects correctness: the portable handle-relative traversal stays authoritative and its totals remain exact.
Acceleration needs a GENERIC_READ volume handle. Measured on this host — the unelevated column on 2026-09-02 against both C: and E:, the elevated column on 2026-09-04 against C::
| Requested access | Not elevated | Elevated |
|---|---|---|
0 / FILE_READ_ATTRIBUTES / SYNCHRONIZE / both | handle opens, but the FSCTL returns ERROR_INVALID_FUNCTION (1) | identical; still 1 |
GENERIC_READ | open is refused with ERROR_ACCESS_DENIED (5) | open succeeds and FSCTL_QUERY_USN_JOURNAL works |
The decisive point is that the lower access levels still report the control codes as absent even when elevated. That is not "insufficient rights" but "the function does not exist at that handle level", so there is no reduced-privilege access level to trade down to. Acceleration being unavailable without elevation is a platform property, not an implementation gap. The whole table is measured at both privilege levels by the volume_access_masks_behave_the_same_at_both_privilege_levels probe rather than inferred from the unelevated result.
A refusal appears as one scan.progress event carrying accelerationRefusalReason (a stable machine code, never localized) and elevationMightHelp. Its coverageEffect is observed rather than incomplete: declining an optimization loses no coverage, and an ordinary unelevated scan must not be reported as partial because of it. elevationMightHelp is true only when privilege is genuinely the cause, so the user is not sent to a UAC prompt that cannot fix the problem — elevation does not help when, for example, the volume is not NTFS.
Pass --elevate to request acceleration explicitly; it asks for one UAC consent and relaunches the process elevated. Destructive operations remain hard-refused in an elevated session by design and are not relaxed just because privilege is higher.
When acceleration does qualify, one bulk read of the volume's NTFS metadata produces a preview of each scan root, reported in the scan summary under acceleration:
"acceleration": {
"used": true,
"preview": {
"entryCount": "37371",
"logicalBytes": "14812812602",
"elapsedMicros": "1058065",
"exact": true,
"authoritative": false
}
}A refusal is reported in the same place as {"used": false, "reason": "not_elevated", "elevationMightHelp": true}.
Two properties of a preview matter:
authoritativeis alwaysfalse. Preview numbers come from a metadata snapshot and carry no reopen recipe, which is what a delete revalidates against. They exist so a large tree can show a total quickly; nothing may be removed on their strength, and the traversal's results supersede them.exactisfalsewhen any record under the root could not be resolved. The size is then a lower bound and must never be displayed as precise.
Measured on this host on 2026-09-02 against E:\Projects\sweepx (14.5 GB, 36,531 objects): the preview completed in 1.06 s where the full authoritative scan took 133 s, roughly 126x faster to a first answer. The authoritative scan itself is not made faster — the preview is additive, and its whole-volume read is a fixed cost of about a second, so it only pays off on large trees.
Preview output is verified against an ordinary directory walk, which reaches the filesystem through a completely different code path; the path set and the summed bytes must match exactly.
Reusing a preview across runs
A stored preview is only useful if something can say it is still true. When a scan writes a preview it also records, inside the same generation, the position of each covered volume's NTFS change journal. The next run re-reads that position: if the volume has not moved, the preview describes the filesystem as it is now and the scan reports loadStatus: "verified_preview" instead of stale_preview.
Verification only ever upgrades a load. No recorded evidence, an unreadable journal, or a volume that did move all keep the previous behavior and add a warning naming the reason:
| Warning | Meaning |
|---|---|
cache.preview.unverified.no_evidence | The stored generation carries no evidence, so there is nothing to re-check. An unelevated run records none. |
cache.preview.unverified.read_failed (N) | Re-reading the journal failed with OS error N. A 5 means run elevated; an 87 means SweepX passed malformed input and is a defect to report. |
cache.preview.unverified.journal_must_rescan | The journal answered that the stored range is no longer covered. Nothing failed; the evidence simply aged out. |
cache.preview.unverified.malformed_evidence | The stored evidence is structurally unusable. |
cache.preview.unverified.unknown_kind (K) | The evidence names mechanism K, which this build does not understand — typically a newer SweepX wrote it. |
cache.preview.unverified.no_mechanism | This build has no change-detection mechanism for the host. |
cache.preview.unverified.changed | The volume demonstrably changed since capture. |
The code after the prefix is stable and safe to match on; any detail is appended in parentheses and is not part of it. Reuse also requires every covered volume to be unchanged, because a half-valid preview would show correct sizes for one part of a tree and stale sizes for another while looking correct.
Two conditions must hold for a preview to verify. Reading the journal needs the same elevated volume handle acceleration needs, so an unelevated run records no evidence and behaves exactly as it did before this existed. And the state directory must be on a different volume from the trees being scanned: the cache's own write is journalled on the volume it records, which advances that volume's change position and leaves the stored evidence stale on arrival. Since the default state directory lives on C:, scanning C: verifies nothing today and reports stale_preview as before. Evidence is stored inside the checksummed generation payload, so editing a token on disk invalidates the whole generation rather than buying a false "unchanged".
Commands that do not exist today
PROPOSED ONLY — NOT IMPLEMENTED
sweepx plan create ...
sweepx plan show ...
sweepx approve ...
sweepx execute ...
sweepx execute ... --dangerously-deleteP3 has library models and fake-execution tests for related concepts, but there is still no plan/approve/execute CLI or Permanent mutation.
site-storage
Reports browser site storage per origin, so a user can decide site by site. Read-only: it never deletes and never pre-selects.
Separate from junk on purpose. Junk means rebuildable; this is R3 browser application state, which the risk taxonomy places at default skip/report, policy may allow an individually selected item. The per-origin breakdown is what makes such a selection possible — without it the only available choice is to clear everything and lose every login.
Two subsystems are attributed, because each keeps one directory per origin:
service_worker_cache_storage— the origin lives in each bucket'sindex.txt, since the directory name is a one-way hash. It is stored as plain UTF-8 and is preceded by its own length, which is how it is told apart from a URL embedded in a neighbouring field: a Workbox site stores the cache nameworkbox-precache-v2-https://gamemap.app/, whose leading length counts the whole name rather than the URL inside it.indexed_db— the origin is in the directory name..leveldband.blobbelong to one origin and are summed, not counted twice.
The reported unit is the full storage key, not a hostname. Chromium partitions third-party storage by top-level site, so one host can hold several mutually invisible sets of data; merging on hostname would present unrelated parties as one row.
fullyAttributed says whether every byte below the subsystem was attributed to some named origin. It is an exact comparison. A 367-byte shortfall was once explained away as a live browser writing between two walks and covered with a tolerance — it was in fact an entire origin the parser was dropping, so the check stays strict.
Local Storage is deliberately absent. Measured 2026-09-05, its 303 origins share twelve LevelDB files many-to-many: one 2.3 MB file held 61 origins, cn.bing.com spanned four files, and 47 origins crossed file boundaries. No file boundary lines up with an origin boundary, and summing an origin's record bytes would not fix it, because LevelDB keeps superseded revisions and tombstones until compaction. Any per-origin figure there would be invented, so none is reported.
Measured 2026-09-05 on this host: Edge Default holds 458.6 MB of CacheStorage across 12 origins (onedrive.live.com alone 200.4 MB) and 276.6 MB of IndexedDB across 43 (www.bilibili.com 236.1 MB, 85% of the subsystem).
Removing one origin's storage
site-storage --trash-origin <STORAGE_KEY> moves that origin's storage to the Trash. One origin per invocation: R3 permits an individually selected item, not a batch.
The key is matched exactly and a hostname is not accepted as shorthand, because a host can own several partitioned, mutually isolated sets of storage. Accepting example.com for https://example.com/^0https://other.test would clear data the user never named.
Three refusals happen before anything is touched, and each leaves the filesystem unchanged:
- Unknown key — exit 2, listing how keys are matched.
- Held database — exit 3. LevelDB guards a database with an exclusive lock on
LOCK; if that lock cannot be taken, the browser has the database open. This is not a theoretical guard: measured 2026-09-05 with 35 Edge processes running, an IndexedDB directory renamed successfully, so the filesystem will not stop the move. The browser would go on writing against a handle whose directory is gone. The probe must open with no write sharing — Rust's default share mode reported 0 of 43 Edge directories held where an exclusive open reported 2. - No interactive terminal in human format — exit 2.
The probe is per directory rather than per browser, because Chromium opens a database only when a site needs it. A directory whose lock cannot be evaluated is treated as held: refusing a removable directory costs nothing, while moving a live database is unrecoverable.
An origin can own several directories — IndexedDB keeps .leveldb and .blob apart — and the Trash offers no transaction, so all directories are checked before any is moved and a later failure is reported as status: partial with the paths that did move. Partial is never rounded to success.
Windows identity revalidation was strengthened for this. same_file previously compared type, length and modification time; a directory's length is zero and its timestamps are writable, so a directory deleted and recreated at the same path satisfied all three while being a different object. Identity now comes from FILE_ID_INFO, the same source the scanner records, and is re-read immediately before each move.
Browsing origins in the TUI
--browse hands the discovered storage subsystem directories to the existing interactive browser, so origins can be inspected one at a time and moved to the Trash with d / Delete.
sweepx site-storage --browseThe rows come from an ordinary scan rather than from the report: the interactive Trash draws its authority from the scanner's native identity and locator, and the model explicitly forbids deriving either from a display path.
The first screen lists each browser's subsystem roots (IndexedDB, Service Worker\CacheStorage); the per-origin directories under them are listed on demand once a root is entered. Root rows keep only as many trailing path components as it takes to stay distinct — the four subsystems do not sit at the same depth, so a fixed cut renders Edge and Edge Dev CacheStorage as the very same text.
--browse requires terminal stdin and stdout, and cannot be combined with --format json / --format ndjson or --trash-origin.
Name-column overflow, full paths, and scrolling
The name column is much narrower than it looks: the five fixed columns take 67 cells first, and only the remainder is split by percentage. On an 80-column terminal the name column is 6 cells, not 42% of 80. Long paths therefore always overflow, and the browser handles that three ways:
- an unselected overflowing row ends in an ellipsis, marking that text was elided;
- the selected row scrolls instead, advancing one cell every 300 ms and repeating after a four-cell gap;
ppins the selected row's full path in the footer, which wraps rather than truncates.
Only the selected row scrolls. Animating every overflowing row at once would move the whole list while the user is trying to read one line of it.
The scrolling window is always exactly the column width. Wide characters occupy two cells, and one that does not fit the final cell becomes a blank rather than being dropped - otherwise the columns to the right shift back and forth as the text moves. A character scrolled half off the left edge is padded for the same reason: dropping it made two consecutive frames render identically, so CJK paths stuttered every second step.
The pinned path is for reading only. It is not execution authority; native identity is still revalidated immediately before any Trash operation.