https://github.com/icanhasjonas/icloud-tools
CLI tool for managing iCloud Drive files on macOS. Replacement for brctl download/evict (removed in Sonoma 14+).
https://github.com/icanhasjonas/icloud-tools
brctl cli command-line dataless-files file-management homebrew icloud icloud-drive macos swift
Last synced: 7 days ago
JSON representation
CLI tool for managing iCloud Drive files on macOS. Replacement for brctl download/evict (removed in Sonoma 14+).
- Host: GitHub
- URL: https://github.com/icanhasjonas/icloud-tools
- Owner: icanhasjonas
- License: mit
- Created: 2026-04-19T12:33:18.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-19T13:34:46.000Z (about 2 months ago)
- Last Synced: 2026-04-19T14:45:10.472Z (about 2 months ago)
- Topics: brctl, cli, command-line, dataless-files, file-management, homebrew, icloud, icloud-drive, macos, swift
- Language: Swift
- Size: 37.1 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# icloud
CLI tool for managing iCloud Drive files on macOS.
Apple removed `brctl download` and `brctl evict` in macOS Sonoma 14+, and `fileproviderctl materialize` in 14.4. No replacement shipped. This tool fills the gap using Foundation APIs that still work from unsigned binaries — no entitlements, no code signing, no sandbox.
## Why
- **rsync deadlocks** on dataless (cloud-only) files: `mmap: Resource deadlock avoided`. You need to download first.
- **No CLI to batch-download** a directory before going offline.
- **No CLI to see** what's local vs cloud-only, or what's eating disk.
- **No CLI to evict** files and free space.
- **"Keep Downloaded" (pin)** is Finder-only — no CLI equivalent existed until now.
## Install
### Homebrew
```bash
brew tap icanhasjonas/tap
brew install icloud-tools
```
### From source
```bash
git clone https://github.com/icanhasjonas/icloud-tools.git
cd icloud-tools
swift build -c release
cp .build/release/icloud ~/.local/bin/
```
Requires macOS 14+ and Swift 6.0+.
## Usage
### Status
Show iCloud status for files. Defaults to cwd if inside iCloud Drive, otherwise the iCloud Drive root.
```bash
icloud status # cwd or iCloud Drive root
icloud status ~/Desktop # specific directory
icloud status -r Documents/ # recursive
icloud status --cloud # only cloud-only files
icloud status --local # only local files
icloud status --sort size # sort by size
icloud status --json # JSON output
icloud status -v # verbose (show resolved paths)
```
Output uses ANSI colors: green = local, dim = cloud/dir, yellow = syncing, cyan P = pinned.
Symlinked paths (e.g. `~/.icloud -> ~/Library/Mobile Documents/com~apple~CloudDocs/`) are resolved automatically and display paths preserve the symlink prefix you passed in.
### Move
Move files with download-first semantics. Dataless files are downloaded before moving, preventing the rsync mmap deadlock.
```bash
icloud mv file.pdf ~/Desktop/ # move single file
icloud mv a.pdf b.pdf dest/ # move multiple into directory
icloud mv -v dir/ dest/ # verbose with per-file progress
icloud mv -f src.pdf existing.pdf # force overwrite (atomic backup+restore)
icloud mv -n src.pdf existing.pdf # skip if exists (no-clobber)
icloud mv -d src.pdf dest/ # dry-run preview
icloud mv -j 5 big-dir/ dest/ # 5 concurrent downloads (default 3)
icloud mv -t 300 huge.zip dest/ # raise baseline timeout to 5 min
icloud mv --json a b dest/ | jq -s . # NDJSON -> array
```
**Force semantics (`-f`)**: the existing destination file is moved to a hidden backup, then the operation runs. On success, backup is removed. On failure, backup is restored. If restore itself fails, the error includes the surviving backup path.
**Merge semantics**: when you move a directory into another directory that already has a same-named subdirectory, contents **merge per-file**. Files present at the destination but not in the source are **never deleted**. Conflicts (same relative path) respect `-f` / `-n` just like single-file conflicts. **Directories at the destination are never deleted or replaced.** Attempting to replace a directory with a file errors out.
### Copy
Same flags as `mv`, plus `-r` for directories.
```bash
icloud cp file.pdf ~/Desktop/ # copy single file
icloud cp -r Documents/ ~/backup/ # recursive directory copy (merge into existing)
icloud cp -vf *.pdf dest/ # verbose, force overwrite
icloud cp -d src.pdf dest/ # dry-run preview
icloud cp -j 10 huge/ dest/ # 10 concurrent downloads
icloud cp --json -r dir/ dest/ | jq -s . # NDJSON -> array
```
### Download
Triggers iCloud download and waits for completion. Files that are mid-download or dataless-but-marked-local are waited on correctly, not skipped.
```bash
icloud download file.pdf # download and wait
icloud download -rv Documents/ # recursive + verbose
icloud download --dry-run -r . # preview, no download
icloud download -j 10 -r Documents/ # 10 concurrent downloads
icloud download -t 300 big.zip # raise baseline timeout to 5 min
icloud download --json -r dir/ | jq -s .
```
### Evict
Makes files cloud-only by removing the local copy. Pinned files are skipped; unpin first.
```bash
icloud evict file.pdf # free local copy
icloud evict -rv old-projects/ # recursive + verbose
icloud evict --dry-run -r . # preview without evicting
icloud evict --json big.zip | jq -s .
```
### Pin / Unpin
`pin` sets the `com.apple.fileprovider.pinned#PX` xattr (same mechanism as Finder's "Keep Downloaded"). Pinned files are protected from automatic eviction on disk pressure and from `icloud evict`.
```bash
icloud pin important.pdf # single file
icloud pin -r Documents/ # recursive
icloud pin --from-tag Green # all files with Finder tag "Green"
icloud pin --from-tag Green+Important -r . # AND: both tags required
icloud pin --from-tag Red --from-tag Blue # OR: either tag matches
icloud pin --dry-run --from-tag Green # preview
icloud unpin important.pdf # allow system to evict
icloud unpin -r Documents/
icloud unpin --from-tag Green
```
Tag filters: repeat `--from-tag` for OR, use `+` inside one expression for AND.
## Parallel downloads & timeouts
`mv`, `cp`, and `download` all support:
- `-j, --max-concurrent ` — concurrent downloads. Default **3**. iCloud handles the concurrency on its end; we fire all `startDownloadingUbiquitousItem` calls and poll the batch.
- `-t, --timeout ` — baseline timeout floor per file. Default **120**.
Effective per-file timeout = `max(baseline, sizeMB × 1.2)`:
| file size | effective timeout |
|-----------|-------------------|
| 1 MB | 120 s (baseline) |
| 100 MB | 120 s |
| 1 GB | 20 min |
| 2 GB | 40 min |
| 10 GB | ~3 hours |
One slow file doesn't stall the batch — each tracks its own deadline.
## Output modes
The same command picks a renderer based on how you invoke it:
| invocation | renderer | example |
|-------------------------------------|------------------|-----------------------------------------------|
| interactive terminal, no `-v` | **TTYQuiet** | `⇣ file (1 MB)` → `✓ file (2.3s)` → `src => dst` |
| interactive terminal, `-v` | **TTYVerbose** | per-file header + ` downloading...` + ` moved to …` |
| piped to another command (non-tty) | **LineStream** | `DL→ / DL✓ / MV✓ / CP✓` one line per event, grep-friendly |
| `--json` | **JSON (NDJSON)** | `{"event":"op.done","src":"…","dst":"…",…}` per line |
### The `=>` arrow
`MV✓ src => dst` and `src => dst` print **only after** post-op `fileExists` + size verification on the destination. If the destination is missing or size-mismatched, the tool emits `opFail` and throws. No lying success.
### JSON events
```bash
icloud mv --json a b dest/ # one NDJSON record per event
icloud mv --json a b dest/ | jq -s . # slurp into an array
icloud cp --json -r dir/ dest/ | jq 'select(.event == "op.done")'
```
Event stream types: `phase.start`, `phase.end`, `discovered`, `download.start`, `download.tick`, `download.done`, `download.fail`, `op.done`, `op.fail`, `op.skipped`, `op.would`.
## How It Works
- **Status:** `URLResourceValues` for download status, file size, allocated size. Dataless files have `fileAllocatedSize == 0 && fileSize > 0`.
- **Download:** `FileManager.startDownloadingUbiquitousItem(at:)` triggers async download; we fire-and-poll all files in the batch concurrently.
- **Evict:** `FileManager.evictUbiquitousItem(at:)` makes files cloud-only.
- **Pin:** `setxattr` / `removexattr` for `com.apple.fileprovider.pinned#PX` (value `0x31`) — the same mechanism Finder uses for "Keep Downloaded".
- **Move/Copy:** downloads dataless files first (parallel, size-scaled timeout), then performs the operation. For directory-into-existing-directory, per-file merge with conflict handling. Destinations are stat'd post-op to verify existence and size before reporting success.
No private APIs. No entitlements. Just Foundation.
## Data-loss covenants
These are why the tool exists in its current shape:
1. **Never lie about success.** `=>` never prints without post-op verification.
2. **Never delete a destination directory.** `-f` only replaces at the file level.
3. **Never swallow syscall errors on the data path.** `setxattr`, `moveItem`, `copyItem`, enumeration, all check their returns.
4. **Download waits for all dataless states.** Not just `.cloud` — `.downloading` and dataless `.local` too.
30 unit tests pin these covenants. Run with `swift test`.
## Limitations
- No per-byte download progress. APFS uses atomic extent swaps, so `fileAllocatedSize` jumps from 0 to full on completion. The tool shows file-level progress (queued / downloading / downloaded) for batch downloads.
- Only works on files under `~/Library/Mobile Documents/`. Desktop/Documents folders only apply if they're synced to iCloud Drive.
## License
MIT