swarmfile
swarmfile drives Swarmfile version-control operations against a running engine: commit, switch/restore, branches, merge, and the rest of the day-to-day project commands. It talks to the engine over its control socket - every subcommand mirrors an operation the Desktop App can do, and every subcommand requires a running engine except completions, demo offline (it changes the host firewall, not the engine, and must work exactly when the engine is what is being cut off), the git helper verbs (git url, git clone and git import, which answer before the engine connection is needed), and the pure Git-habit shortcuts (push/pull/clone/rebase/cherry-pick/rm/mv/cp/mkdir/touch/worktree/grep/init/remote/clean/reflog/mergetool/difftool/shortlog/submodule/bisect/gc/prune/fsck/archive/notes), which work standalone. add, stash, reset, and the executable clone <id> <path> / clone … --checkout / clone … --into forms are the exception among the shortcuts - they map onto real commands, so they need a running engine too (see the Git-habit shortcuts note below).
If the engine isn't running, start it (or the Desktop App) first, or use the standalone swarmfile-doctor binary to diagnose connectivity without one.
swarmfile comes with the desktop app on every platform: /usr/bin/swarmfile from the Linux .deb, /usr/local/bin/swarmfile on macOS (a symlink into Swarmfile.app, so desktop updates carry it too), and swarmfile.exe in the Windows install folder (C:\Program Files\Swarmfile), which the installer adds to the machine PATH. There is no separate CLI download - install the desktop app and open a new terminal. An install from before the tools shipped (or a macOS install that came from the .dmg rather than the .pkg) needs one full installer run to get them and the PATH entry - Diagnostics → Reinstall does that from the tray; on macOS the tray also repairs the /usr/local/bin symlinks at launch where the directory is writable.
swarmfile doesn't include a search subcommand - search needs its own hub-authenticated round trip independent of the control socket, so it lives in the standalone swarmfile-search binary instead.
Global flags#
These apply to every subcommand.
| Flag | Description |
|---|---|
--cache-dir <path> | Engine cache dir (where control.json lives). Respects $SWARMFILE_CACHE_DIR. Default ~/.cache/swarmfile (macOS/Linux), %LOCALAPPDATA%\swarmfile (Windows). |
--json | Print raw JSON instead of a human-readable summary. |
--mount <id> | Target this mount instead of whichever one is current - see mounts below. Has no effect on mounts subcommands themselves, which address by their own id argument instead, nor on workspace switch, which changes the whole engine's active workspace rather than one mount. |
--no-wait | Don't wait for a long operation: print its id and exit 3 at once. |
These are global flags, so they work on either side of the subcommand:
swarmfile --json statusandswarmfile status --jsonare equivalent, as areswarmfile --mount m2 statusandswarmfile status --mount m2. That holds for the git-habit shims too -swarmfile rm foo --jsonis read as the flag, not a path.
Exit codes#
Scriptable commands separate "you need to do something" from "something failed", so a CI or pre-flight script can branch on it:
| Code | Meaning |
|---|---|
0 | Success - including the Git-habit shortcuts whose intent the product already satisfies (push, pull): nothing to do, so nothing failed. |
1 | Any other failure: an unreachable engine, a hub transport error, and the shortcuts whose operation has no equivalent here. |
3 | A long operation is still running - after --no-wait, or when you pressed Ctrl-C to detach. It carries on in the engine; follow it with op status / op wait. |
2 | A refusal a script can act on, or a CLI usage error (an unknown command, flag, or argument). With --json, refusals print their response body as before, and usage errors print {"code":"usage_error","kind":"…","error":"…"} instead of clap's prose - so a script can tell a typo apart from an actionable refusal. |
A command page can assign 3 its own meaning where there is no long operation to follow - git index exits 3 when the hub couldn't be reached, for example.
The translated shortcuts (add, stash, reset --hard <rev>, clone <id> <path>) return the exit code of the command they map to, not a shortcut code.
Git-habit shortcuts honor --json: instead of the sentence they print {"code":"already_satisfied"|"not_applicable","exitCode":N,"hint":"…"}.
The confirm-gated verbs all take -y/--yes, and most also accept -f/--force as a synonym - except git enable, and the verbs with a separate safety-gate --force below - stash drop/clear, changelist discard, branch prune, trash purge, project delete, sync discard, git enable (one-way, so it never proceeds without it), ignore-clean, and (unless --dry-run) cache reclaim/scratch gc. Without it, a non-interactive stdin or --json refuses ({"code":"confirmation_required"}, exit 2) rather than destroying work; the estimate-only verbs (ignore-clean, cache reclaim, scratch gc) take --dry-run to preview without confirming or deleting. Two prompt-gated verbs are the exception and proceed without --yes in the right context: hydrate start and workspace switch both proceed under --json or a non-interactive stdin (an interactive terminal still prompts). Both are resumable/reversible operations rather than destructive ones, and --yes only skips the interactive prompt.
A few verbs have a separate -f/--force meaning "proceed despite a safety gate" - mounts close (open changelists), scratch clear (project still open), and ignore-clean (-y skips its prompt, -f also overrides its recent-foreign-edit gate).
Commands that currently use exit 2: mr status (open, not yet approved - the CI review gate), conflicts resolve --auto / --all --auto (a human must choose - a request that fails mid-sweep is exit 1 and takes precedence), lock (already held), mounts close (open changelists, or the boot mount), acl mode set / acl entry … (forbidden / not_found / plan_restricted), unlock-request respond/cancel (already resolved, or not yours), workspace switch (another switch running, a changelist open, or a drive open on another project - cross_project_mount_open), checkout with no target, ignore-clean --yes when a candidate was recently edited by someone else (unless --force) or when any delete failed, and hydrate start / fetch when a job is already running, and any confirm-gated verb without --yes (the confirmation_required refusal above; ignore-clean --yes keeps its own cross-machine gate).
Version control#
See Version Control and Branches & Merging for the conceptual walkthrough - this section is the flag-by-flag reference. If you're arriving from Git, Coming from Git maps your habits onto these commands.
Git-habit shortcuts. Typing a Git command that has no Swarmfile equivalent -
push,pull,clone,rebase,cherry-pick,rm,mv,cp,mkdir,touch,worktree,grep,init,remote,clean,reflog,mergetool,difftool,shortlog,submodule,bisect,gc,prune,fsck,archive,notes- prints a one-line pointer to the real workflow instead of an error (e.g.pushexplains that saves sync automatically,rm/mvpoint at doing it on the mounted drive,worktreepoints atswarmfile mounts open,greppoints atswarmfile-search,mergetoolpoints atconflicts resolve --tool, andcloneis argument-aware - a public-project URL, a/s/<token>share link, aswarmfile://mount?project=…deep link, or a lone project id each get the right next step;clone <project-id> <path>actually opens a mount - add-b <branch>to pin it to that branch, exactly likemounts open --branch, or add--checkoutto write a plain directory instead (seematerialize);clone <public-project> --into <org>/<project>copies a public release into a project, encrypting blocks on this machine - the way to get a public release into a private project, since the web "Copy to my org" fork only produces public ones. Add--tag <tag>to pick a release,--path <p>(repeatable) to copy a subtree, and--new(with optional--public) to create the destination project first. It runs as a long operation (--no-wait,swarmfile op status|wait|cancel; per-file progress; a cancel resumes from what's done), and its refusals (unknown slug or tag, no write access, a protected branch, a name collision) exit2while hub or transport failures exit1. These shortcuts work even when the engine isn't running (the executablecloneforms need it).Three shortcuts have a real destination and run it instead of just advising:
stash(andpush/save) →changelist park,stash list/show→changelist parked,stash pop/apply→changelist resume,stash drop/clear→changelist discard(stash listnumbers entriesstash@{n};stash drop stash@{n}ordrop ndiscards just that one);add→ opens a changelist if none is open (idempotent - saves then stage into it automatically, and this mount goes into staged mode until youcommitorchangelist cancel); andreset --hard <rev>→restore <rev>(one undoable commit).add <path>exits 1 if the path doesn't resolve, so a typo doesn't silently pass. Parked work is a set, not a stack: a barepop/applyresumes all parked work for the branch, andpop <n>(orpop stash@{n}) resumes just that entry and leaves the rest parked - an entry parked on another branch is refused, since parked work resumes on its own branch (switch to it instead). If an entry's changeset was closed on the hub while it sat parked, resuming re-creates it and its staged files under a new id, and the result names the new id. A baredroporclearabandons all of it - so discards ask first (or take--yes). On success these print a sentence, not raw JSON. Barereset --hard,--soft/--mixed, and bareresetstill print guidance, since there is no unstaged state to discard. These three need a running engine.
Referring to a commit. Every command that takes a commit (show, restore, revert, describe, diff, and the commit form of checkout) accepts any of: a commit number (482), the #N form the log prints (#482), HEAD, HEAD~N / HEAD^ (N commits back in the current branch's log), a tag name, a branch name (its head), a changeset id, or a 64-hex commit hash. A seq:/head:/branch:/tag:/changeset:/hash: prefix forces which kind is meant. Resolved client-side to the commit number before the request.
commit#
Open a changelist (if none is open) and submit it in one step.
| Flag | Description |
|---|---|
-m, --message <String> | Commit message |
--amend | Rename the most recent commit instead of creating a new one (like git commit --amend). Message-only and non-destructive - contents and history are untouched. Requires -m. Equivalent to describe on the latest commit. |
swarmfile commit -m "Fix layout regression in the deck"
swarmfile commit --amend -m "Fix layout regression in the title deck"
changelist#
Explicit changelist control (Mode B: staged atomic submit) - for when you want to stage multiple saves before committing them atomically, rather than using commit's one-step flow. A mount can hold several open changelists at once (Perforce's -c model) - see Multiple changelists at once for the conceptual walkthrough.
| Subcommand | Flags | Description |
|---|---|---|
open | -m, --message <String> | Open a new changelist and make it current; subsequent saves stage into it until submit/cancel/switch. Never fails if one is already open - opening a second doesn't close the first. |
submit | --id <changeset> | Atomically apply a changelist. Defaults to the current one; --id targets a specific one without switching to it first. |
cancel | --id <changeset> | Abandon a changelist. Defaults to the current one; --id targets a specific one without switching to it first. |
status | - | Show whether the current changelist is open and what's staged, plus any files blocked by an unresolved conflict - the same changelist + conflicts pair status --changes renders. |
list | - | List every open changelist, * marking which one is current. |
current | <id> | Switch which open changelist is current. |
reopen | [path], --to <changeset>, --from <changeset> | Move staged file(s) to a different open changelist without unstaging/restaging. With path, moves that one file. Without it, moves every staged file - from the current changelist, or from --from <changeset> if given - to --to. --from applies only without a path (a path with --from is a usage error). |
park | - | Set aside every open changelist on this mount (kept, and kept alive on the hub) so the branch/project can be left without abandoning the work. |
parked | - | List parked changelists with age + files. |
resume | - | Restore parked work for the mount's current branch. |
discard | --changeset <id>, --yes | Abandon parked work (one with --changeset, else all). Asks first (it cannot be undone); --yes skips the prompt. |
swarmfile changelist open -m "Reorganize asset folders"
# ...edit and save files...
swarmfile changelist status
swarmfile changelist submit
swarmfile changelist open -m "Swap all Act 2 plates to final grade"
swarmfile changelist open -m "Quick fix: wrong LUT on shot 12"
swarmfile changelist list
swarmfile changelist current a1b2c3d4
swarmfile changelist reopen "Renders/Act2/shot12.mov" --to e5f6a7b8
swarmfile changelist submit --id e5f6a7b8
switch#
Switch which branch this mount works on (like git switch) - hot and in-process, no engine restart. Same operation as branch switch. "This mount" is the --mount one, else the current mount (mounts current): the branch checks, -c's create, and the switch itself all act on that same mount. A mount that shares another mount's branch context can't switch on its own and is refused (shared_branch_context) - -c refuses before creating anything. The success line names the branch the engine reports it switched to.
| Argument / Flag | Description |
|---|---|
[name] | Branch to switch to. Omit to return to the project's default branch (usually main). |
-c, --create | Create the branch first (forking from the current branch), then switch to it - like git switch -c. Errors if it already exists. |
Refused while any changelist is open on that mount (exit 2 on the default mount; on any other mount the switch fails with the same reason, exit 1); submit or cancel all of them first.
swarmfile switch look-dev-b
swarmfile switch # back to the default branch
swarmfile switch -c feature-x # create + switch
restore#
Restore a scope (the whole project, a folder, an entry, or a mount-relative path) to a past commit - a true tree snapshot, landed as one undoable commit (like git restore, but whole-project by default). Locked files → exit 2, and a file the restore would change that has an unresolved conflict refuses with conflict_unresolved - resolve it first.
| Argument / Flag | Description |
|---|---|
[seq] | A commit to restore to - #N, HEAD~N, a tag, a changeset id, or a 64-hex hash (see "Referring to a commit"). A branch name restores this mount's branch to that branch's head tree (it does not switch branches - that's switch). |
--at-seq <N> | Commit number to restore to - instead of [seq], not with it (both at once is a usage error). |
--path <path> | Restore only this mount-relative path. |
--folder-id <id> | Restore a specific folder by ID. |
--entry-id <id> | Restore a specific entry by ID. |
swarmfile restore 482 # whole project back to commit #482
swarmfile restore 482 --path shots/seqA # just that folder
--json prints the engine's response plus targetSeq - the commit number the CLI resolved your ref to; the engine's own seq is the NEW commit the restore landed as.
checkout#
A Git-habit shim mirroring git checkout's overloading - prefer switch / restore, which say exactly what they do.
| You type | What happens |
|---|---|
checkout <branch> | Switches to that branch (→ switch). |
checkout <N> (a number, HEAD, HEAD~N) | Restores to that commit (→ restore). |
checkout <tag> / <hash> / <changeset-id> / a prefixed ref (tag:x, seq:5, hash:…) | Restores to that commit - resolved exactly as restore resolves it. branch:x switches to x. |
checkout -b <name> [<target>] | Creates a branch and switches to it - like git checkout -b. Forks from <target> (a branch, tag, or commit - same rules as branch create --from) or --at-seq <N>, else from the current branch. --path/--folder-id/--entry-id don't combine with -b (a branch is whole-project). |
checkout --at-seq <N> [--path …] | Restores the version at that sequence number (the --at-seq form). Not combined with a <target>. |
A bare number restores that commit unless a branch of that name exists - an existing branch wins even when its name is commit-shaped, as in git, and the CLI prints a note when it does. Force the branch with
switch <name>, or force the commit with ahash:/seq:prefix.
show#
Show one commit: its message, author, time, and the files it changed (like git show). Whole-file - Swarmfile versions binaries, so there's no line-level diff.
| Argument | Description |
|---|---|
<rev> | A commit - number, #N, HEAD, HEAD~N, or a tag (see "Referring to a commit" above). |
swarmfile show 482
swarmfile show HEAD # the latest commit
swarmfile show HEAD~2 # two commits back
swarmfile show v1.0 # a tag
show-tree#
Fetch and print a tree object by its content-addressed cid (like git ls-tree) - one level, not recursive: each entry's kind (d directory, f file, l symlink), name, and content cid. Get a cid from swarmfile show <rev>'s tree line. A directory large enough to be stored segmented is listed a segment at a time - each segment is its own fetch, never one unbounded request for the whole directory. A project member's listing is filtered against the selected mount's branch view (the same ACLs a normal listing applies), so a cid taken from an old commit can come back partial or empty; owners and service callers see the object unfiltered.
| Argument | Description |
|---|---|
<cid> | A tree object's content-addressed cid (64 lowercase hex characters). |
swarmfile show-tree a1b2c3...
revert#
Undo a commit by landing a new commit that restores the prior versions of the files it changed (like git revert) - non-destructive, itself undoable, and it doesn't touch newer commits. Locked files → exit 2, and a file the revert would change that has an unresolved conflict refuses with conflict_unresolved - resolve it first.
| Argument | Description |
|---|---|
<seq> | The commit to undo - a number (#N), HEAD~N, a branch, a tag, a changeset id, or a 64-hex hash (see "Referring to a commit"). |
swarmfile revert 482
rollback#
Restore one file to an earlier version, keeping the current version in history - the CLI form of the Desktop App's version picker. File-scoped and non-destructive, unlike revert, which undoes a whole commit.
Requires the file's write lock, and refuses (exit 2) if the file has uncommitted local changes, is locked, has an unresolved conflict, or changed since the version was chosen. Get the version id from swarmfile blame.
| Argument / Flag | Description |
|---|---|
<path> | Mount-relative (or absolute) path to the file. |
--at <id> | The version's history id - the #N column from swarmfile blame <path>. |
swarmfile blame shots/seqA/plate_v3.exr # find the version id
swarmfile rollback shots/seqA/plate_v3.exr --at 4182
describe#
Name, rename, or clear a commit's message after the fact - including an auto-saved commit that landed with no message (like git commit --amend -m, but for any commit and non-destructive). Author-or-admin only.
| Argument / Flag | Description |
|---|---|
<ref> | The commit to describe - the full ref vocabulary (482/#482, HEAD~N, a branch, a tag, a changeset id, or a 64-hex hash). |
-m, --message <String> | The new message. |
--clear | Remove the commit's message. |
Exactly one of -m / --clear is required. Alias: reword.
swarmfile describe 482 -m "v2 delivery to client"
swarmfile describe 482 --clear
diff#
Show what changed. With no argument (or a branch name) it previews what merging a branch into its base would change (like git diff main...); with two commits it shows the files that changed between them (like git diff A B); with one commit (any ref that isn't a branch) it shows what changed from that commit to the current branch head - git's diff <commit> compares against the working tree, and since saves sync as they happen here, the head is the closest equivalent. Whole-file: there's no line-level diff for a binary. Read-only - it writes nothing.
| Argument | Description |
|---|---|
(none) / [branch] | Branch preview - the given branch (or this mount's) vs its base. A branch preview is only meaningful for a non-main branch (main has no base → 400). |
<commit> | That commit vs the current branch head - any non-branch ref (HEAD~3, a tag, a hash, tag:x, …). |
<A> <B> | Two-commit diff - files changed between commit A and B. Each accepts the full ref vocabulary: a number, #N, HEAD, HEAD~N, a branch, a tag, a changeset id, or a 64-hex hash (prefixes force a kind). |
swarmfile diff # this branch vs its base
swarmfile diff look-dev-b # a named branch vs its base
swarmfile diff HEAD~3 HEAD # what changed over the last three commits
swarmfile diff v1.0 v2.0 # between two tags
blame#
Who last changed a file, plus its version history with authors (like git blame, but whole-file - one row per saved version, not per line, since content is opaque).
| Argument | Description |
|---|---|
<path> | Mount-relative (or absolute) path to the file. |
--branch <name> | Read this path's history on another branch (read-only) instead of the selected mount's - that branch's metadata is synced on demand, and the path must exist there. |
swarmfile blame shots/seqA/plate_v3.exr
changesets list#
The commit log, newest first, scoped to the branch this mount is on (or another named branch, read-only).
| Flag | Description |
|---|---|
--limit <N> | Default 50 |
--offset <N> | Default 0 |
--branch <name> | Read another branch's log without switching mounts. Omitted = the selected mount's branch. |
branch#
Branch management.
| Subcommand | Flags | Description |
|---|---|---|
list | - | Active branches for this mount's project. Each row carries its head commit's hash (headCommitHash; null when no head hash is recorded). |
create | <name>, --from <REF>, --from-commit <seq>, --from-tag <tag> | Fork a new branch (defaults to forking from the selected mount's branch's live head, like switch -c). --from takes the full ref vocabulary: a branch forks that branch (an empty one included), a tag forks its commit, and a seq/hash/changeset id forks that commit with the source branch derived from it. --from-commit/--from-tag are the explicit forms; combining a branch --from with one of them is allowed and the hub refuses the fork if the point belongs to a different branch (branch_mismatch), while a second explicit point (a tag/seq --from plus --from-commit/--from-tag) is refused up front. |
switch | [name] | Switch which branch this mount (--mount, else the current mount) works on. Omit name to switch back to the project's default branch (usually main). |
archive | <name> | Soft-archive a branch (the project's default branch cannot be archived). Exit 2 on an actionable refusal: unknown branch, the project's default branch, a protected branch archived by a non-admin, a branch another active branch is based on (branch_has_active_children), or one still named by an open merge request (branch_has_open_mr). A merged/closed MR doesn't block, so mr merge --delete-branch still works. |
merged | - | Branches with nothing left to land - a branch-vs-base diff with no changed files and no conflicts (the same computation diff <branch> shows). The project's default branch is never listed, a branch whose diff can't be computed is excluded rather than assumed merged, and a branch an open mount is working on is marked in use. |
prune | --yes | Archive every prunable branch merged lists, after naming them and confirming. Never archives a branch an open mount on this machine is on. --yes skips the prompt; --json without --yes returns a confirmation_required error instead. The result lists archived / refused / failed by name. Exits 2 if any branch was refused (protected, etc.), 1 on a transport error. |
protect | <name>, --required-approvals <N> | Refuse a direct merge into this branch and every direct write to it (saves/edits/renames/moves/creates/deletes, staging/committing, rollback, conflict resolution, LFS reflect) - changes land only through an approved Merge Request. --required-approvals (1-10, default 1) sets this branch's approval floor - an unprotected branch otherwise requires none; a matching protection rule's own count can raise it further (the effective count is the MAX of both). The CI-checks-required gate (--require-checks-to-pass) is a protection-rule-only flag, not available per-branch here - protect via a single-branch (no-wildcard) rule instead if you need it without a pattern. Org-admin-gated. |
unprotect | <name> | Clear a branch's protected flag. Org-admin-gated, same as protect. |
set-commit-mode | <name>, [mode] | Lock (or unlock) this branch to a commit mode - overrides the project's own lock, if any, while set. mode is auto-commit, staged, or omitted/unset to clear it back to unenforced-at-branch-scope (the project's lock, if any, still applies). Already-open sessions/changelists are grandfathered: this only affects the next commit/changelist open, never one already in progress. Org-admin-gated: exit 2 on forbidden (403) or an unknown branch (404); any other failure is exit 1. |
branch switchis hot - no engine restart. It's refused (exit 2) while any changelist is open; submit or cancel all of them first. With the global--mount <id>, it retargets that mount only (a mount sharing another mount's branch context can't switch on its own and is refused withshared_branch_context- open an independent mount instead); a branch that doesn't exist can't be switched to.branch createnever touches the running mount either, even with--from-commit/--from-tag- creating a branch doesn't change which branch this mount is checked out to.protect/unprotectexit 2 onforbidden(non-admin caller),not_found(unknown branch), or (protectonly) an out-of-range--required-approvals(400) - distinguishable business states, not a crash - and exit 1 on anything else.
swarmfile branch create recover --from-commit 482
swarmfile branch create recover --from-tag v1.0
protection-rule#
Glob-pattern branch protection - additive on top of branch protect/unprotect's per-branch flag. A rule like release/* protects every branch whose name matches it, including ones created after the rule was added; a pattern with no wildcards is just another way to protect one exact branch.
| Subcommand | Flags | Description |
|---|---|---|
list | - | Protection rules for this mount's project. Org-admin-gated, like create/delete: exit 2 on forbidden. |
create | <pattern>, --required-approvals <N>, --require-checks-to-pass | Create a rule. * matches any run of characters, ? matches exactly one. --required-approvals (1-10, default 1) is this rule's contribution to the approval floor for every branch it matches - see branch protect's row above. --require-checks-to-pass refuses a merge unless every CI check that has reported a run at the request's exact current head shows its latest run passing. A check whose latest run is at an earlier commit is not carried forward as still-required; if no check has reported at the head at all, the gate fails closed rather than passing vacuously. This flag exists only here, at the rule level; there's no per-branch equivalent on branch protect itself. Org-admin-gated: exit 2 on forbidden (403), invalid_pattern/invalid_required_approvals (400), or pattern_taken (409); exit 1 on anything else. |
delete | <pattern> | Delete a rule by its exact pattern text. Exit 2 on forbidden or not_found, same convention as create. |
add-reviewer | <pattern>, <principal-id> | Add an auto-reviewer - every merge request opened against a branch this rule matches automatically requests a review from this principal. Additive to manually-requested reviewers, never a replacement. Idempotent. Org-admin-gated. |
remove-reviewer | <pattern>, <principal-id> | Remove an auto-reviewer from a rule. A no-op if they weren't one. Org-admin-gated. |
list-reviewers | <pattern> | List a rule's configured auto-reviewers. Human output shortens each id (user 1a2b3c4d…); --json carries the full ids that add-reviewer/remove-reviewer take. Org-admin-gated. |
swarmfile protection-rule create "release/*" --required-approvals 2 --require-checks-to-pass
swarmfile protection-rule list
swarmfile protection-rule add-reviewer "release/*" <principal-id>
swarmfile protection-rule list-reviewers "release/*"
swarmfile protection-rule delete "release/*"
tag#
A project-scoped, immutable named pointer to a commit - "branch from tag" is "branch from commit, resolved via a name." A tag also pins the commit it names against history retention, so checkout/restore to it keeps working while the tag exists; deleting the tag releases the pin. There is no move/repoint subcommand: to repoint a name, delete it and create it again. (With git access on the project, a lightweight tag can also be moved or deleted over the git remote - see Cloning a Project with Git.)
| Subcommand | Flags | Description |
|---|---|---|
list | - | Tags for this mount's project. Each row carries the tagged commit's hash (commitHash; null when no hash is recorded for the tagged commit). |
create | <name>, [REF], --at <ref>, --at-seq <seq>, --on-branch <branch> | Tag a commit. REF takes the full vocabulary (N/#N, HEAD~N, a branch, a tag, a changeset id, a 64-hex hash; prefixes force a kind); --at-seq N is the bare-number shorthand. Omit the ref to tag the CURRENT head of the selected mount's branch, or of --on-branch (which must name an existing branch - checked with every form, --at-seq included). --from remains a hidden alias of --on-branch. Errors if the branch has no commits yet. Needs project-wide write (see Permissions); exit 2 on that refusal (403). |
delete | <name> | Delete a tag. Real delete, not soft - a tag has no archived state. Same access and exit 2 on 403 as create. |
swarmfile commit -m "final render pass"
swarmfile tag create v1.0
swarmfile tag create v0.9 --at-seq 482
swarmfile tag list
swarmfile tag delete v1.0
ref#
A thin read-only grouping over branches and tags - for the two questions that need both listings at once. branch and tag remain the full commands for acting on either.
| Subcommand | Flags | Description |
|---|---|---|
list | - | Every named pointer: branches (the current one marked *) and tags, together - each carrying its head/tagged commit hash. |
resolve <name> | - | Say what a ref resolves to under the same resolver every other command uses - the kind (branch, tag, seq, head, hash, changeset) and commit number restore/checkout/log would act on - with a note (alsoMatches in --json) when the name also matches another kind: a branch wins over a same-named tag, and tag:<name> selects the tag. Exit 1 if it doesn't resolve. |
swarmfile ref list
swarmfile ref resolve v1.0
release#
Publish a tag as an immutable, public release - a static, cached snapshot of that tag's tree, served anonymously for browsing and gated behind a free account for full downloads. Requires the project to already be plaintext-tier and public (every public project is plaintext-tier, on any plan - see Billing and plans); publishing enqueues a combined-archive build, so a fresh release starts pending and moves to ready (or failed) asynchronously. list/create/preflight are scoped to this mount's own project; fetch is not - it pulls from ANY public project by org/project slug, whether or not this engine has it mounted, since a public release needs no org membership, only a real signed-in account.
A release can also carry assets: named binaries attached to it without living in the project tree (no manifest entry, no combined archive, no clone history). They are served anonymously at /public/<org>/<project>/assets/<name> on the hub, which is how build artifacts ship alongside the tagged source; attach uploads one, assets lists them, detach removes one, and re-attaching the same name replaces it.
| Subcommand | Flags | Description |
|---|---|---|
list | - | Releases published for this mount's project, each with the id delete takes, its status (pending/ready/failed), and the tagged commit's commitHash (null when no hash is recorded for the tagged commit). |
preflight | <tag> | Report exactly what create <tag> would do, publishing nothing: the gates (public/plaintext, your authority, verified email), the file count and size that would become public, the paths .swarmfile/publicignore.yml keeps out, the paths .gitignore/.swarmfileignore hide from the web browser that would still publish, and whether the tag's policy differs from the default branch's current one (the fixed-the-policy-but-didn't-re-tag trap). Exit 0 when the tag is publishable, 2 when it isn't (the reason is printed), 1 when the check itself failed. |
create | <tag> | Publish a tag. Re-running this on an already-published tag is a no-op unless the prior attempt failed non-permanently, in which case it retries. Exit 2 on a defined refusal: not plaintext+public (409), the tag's prior publish failed permanently (409 - publish a new tag instead; a terminal publicignore_invalid from a committed .swarmfile/publicignore.yml lands here, so fix the file and publish a new tag), the tag doesn't exist (404), the project isn't provisioned on the hub yet (501), or you lack the access to publish (403 - see Permissions; this includes a folder in the tag you're denied read on, and an unverified email). |
delete | <id> | Unpublish. Real delete, not soft, and needs the same access as create (see Permissions). Find id via list's output first. Exit 2 on 404 (already unpublished, or a typo'd id) or 403 (you lack the access to publish, so you can't unpublish either). |
attach | <tag> <file>, --name <name>, --content-type <type> | Upload a local file as an asset of the release named by tag (see list). The file's basename is the asset name unless --name overrides it; --content-type overrides the type derived from the file name. The engine computes the SHA-256 the hub displays. Re-attaching the same name replaces it. Files up to 100 MiB stream through the hub; larger files (up to 5 GiB) upload directly to the project's storage with a presigned URL. Exit 2 on a defined refusal: a bad name (400), no authority (403), a missing release (404), too many assets (409), a file over the 5 GiB cap (413), or a project bucket that cannot accept a direct upload when the file is over 100 MiB (409 presign_unavailable). |
assets | <tag> | List the release's attached assets (name, size, content type, SHA-256). |
detach | <tag> <name> | Remove one attached asset. Exit 2 on 404 (no such asset) or 403 (no authority). |
fetch | --org <slug>, --project <slug>, --tag <name>, --path <path>, --out <path>, --into <org>/<project> | Fetch one file from the release's manifest, or (if --path is omitted) the whole release as a combined .tar. --org/--project are slugs, matching the public URL shape, not internal ids. With --into, nothing is written to disk: the release is copied straight into that project by the engine - the same client-side copy as clone … --into (see Version control), which seals the blocks on this machine so a private destination works - --out is then not required, and --path narrows the copy exactly as it narrows an archive. The copy runs as a long operation (--no-wait, `swarmfile op status |
Browsing a published release anonymously (no account, no CLI) is a web-hosted page, not a CLI surface - the same split share describes for share links.
swarmfile tag create v1.0
swarmfile release preflight v1.0
swarmfile release create v1.0
swarmfile release attach v1.0 dist/widgets-1.0-macos-arm64.tar.gz
swarmfile release assets v1.0
swarmfile release detach v1.0 widgets-1.0-macos-arm64.tar.gz
swarmfile release delete rel_9f2a
swarmfile release list
swarmfile release fetch --org acme --project widgets --tag v1.0 --out widgets.tar
swarmfile release fetch --org acme --project widgets --tag v1.0 --path docs/readme.md --out readme.md
swarmfile release fetch --org acme --project widgets --tag v1.0 --into myorg/private-widgets
merge#
Merge this mount's branch into the branch it was forked from - its parent (usually main, but a branch forked from a non-main branch merges back into that one). The target is implicit: a branch already knows its own parent, so there's no target flag. A merge that would change an entry with an unresolved conflict is refused with conflict_unresolved - resolve it first, then re-run.
| Flag | Description |
|---|---|
--from <branch> | Merge FROM this branch instead of the selected mount's own - land a branch you aren't standing on. The target is still the source's parent (main, or whatever it was forked from). |
--record-conflicts | Record entry_conflicts rows for interactive resolution instead of only listing them on conflict. |
--diff3 | On a merge conflict, batch-resolve every text-file content conflict with the engine's three-way merge before giving up, then retry the merge once. Never lands a partial merge on its own. Overrides a project's .swarmfile/merge.yml diff3: false. Mutually exclusive with --no-diff3. |
--no-diff3 | Force diff3 auto-resolve off for this invocation, even if the project's .swarmfile/merge.yml declares diff3: true. Mutually exclusive with --diff3. |
--dry-run | Print what the merge would change - added, modified, deleted, renamed, and conflicting files, the same read-only diff swarmfile diff <branch> shows - and stop. Nothing is staged and no merge lock is taken. |
--wait | Large merges apply in the background; the command otherwise returns as soon as the merge is accepted. Block and poll until the apply finishes, printing each phase. Gives up after ten minutes with exit 2 and explains how to resume or roll the merge back. |
swarmfile branch create feature-x
swarmfile branch switch feature-x
# ...work, commit...
swarmfile branch switch
swarmfile merge --record-conflicts
mr#
Open, list, inspect, review, and land Merge Requests - the propose → review → approve → merge gate that sits on top of merge above.
| Subcommand | Flags | Description |
|---|---|---|
create | --title <title>, --from <branch>, --body <text>, --draft | Open a merge request from --from into its base branch. --from defaults to whatever branch this mount is currently on. Prints the branch-vs-base diff preview (same as diff <branch>) before opening it, so you see what a reviewer will see. --draft opens it as a draft - reviewable and commentable, but mr merge refuses it, and the usual "new merge request" notification never fires for it at all (not deferred - marking it ready later doesn't retroactively send the one it skipped). |
list | --status <open|merged|closed|all> (default open), --limit <N>, --label <label-id>, --assigned-to <principal-id> | Merge requests for this mount's project. --limit is a hint the hub caps at 500 regardless. --label/--assigned-to filter server-side - see label list/assignees, requested reviewers, and labels for where the ids come from. --assigned-to matches the assignee role specifically, not a requested reviewer. |
status | <number> | Branch names, who opened it, approval state, per-reviewer verdicts, assignees/requested-reviewers/labels, and changed-file count for one merge request, by its display number (the "3" in "MR-003"). Exit code doubles as a CI gate: 0 approved, 2 open but not yet approved, 1 any other error. |
diff | <number> | The merge request's live diff - the files it would change (A/M/D/R) plus any conflicts, rendered exactly like diff above. Read-only, recomputed fresh by the hub on each call. If the MR's source branch was archived/deleted the hub has no diff and this says so, rather than printing an empty list that reads as "no changes." |
approve | <number>, --note <text> | Approve a merge request. Refused (self_review) if you opened it yourself. |
request-changes | <number>, --note <text> | Request changes on a merge request - blocks merge below until a later review approves it instead. Same self-review restriction as approve. |
review-comment | <number>, --note <text> | Leave a non-blocking "Comment" review - shows up in the review history but never counts toward the approval floor and never blocks merge. Unlike approve/request-changes, allowed on your own merge request. Distinct from comment/comments below, which post to the discussion thread, not the formal review history. |
ready | <number> | Mark a draft merge request ready for review - lifts merge's draft refusal, and notifies the merge request's current assignees and requested reviewers (mr_ready). Opening it as a draft suppressed the one-time "new merge request" notification, which is not re-sent. One-directional: there's no way back to draft. Refused (not_draft) if it isn't currently a draft, or (invalid_state) if it isn't open. |
merge | <number>, --record-conflicts, --delete-branch, --diff3, --no-diff3 | Land an approved merge request - same conflict detection as merge above, including the --diff3/--no-diff3 override flags. Refused with not_approved unless enough reviewers have approved (zero on an unprotected branch; a protected target branch or rule raises the floor, 1-10), nobody's latest verdict is "request changes," and (if the target requires it) CI checks pass. Refused with is_draft while the merge request is still a draft - run mr ready first. --delete-branch archives the source branch once the merge has actually landed - opt-in, off by default; a failed archive (e.g. the branch got protected in the meantime) never fails the merge itself, just prints a warning. |
close | <number> | Close a merge request without merging it. |
reopen | <number> | Reopen a closed merge request. Refused (invalid_state) unless it's currently closed - a merged merge request can never be reopened. |
comment | <number>, --message <text> | Post a comment to a merge request's discussion thread - the same thread the web dashboard's merge request page shows. |
comments | <number> | List comments on a merge request's discussion thread. |
assign | <number>, <principal-id> | Assign a principal (user or group id - see admin nodes/the web dashboard's people picker for ids) to land this merge request. Idempotent: assigning someone already assigned is a no-op, not an error. |
unassign | <number>, <principal-id> | Unassign a principal. A no-op if they weren't assigned. |
request-review | <number>, <principal-id> | Request a review from a principal - distinct from assign (who lands it). Idempotent. |
unrequest-review | <number>, <principal-id> | Withdraw a review request. A no-op if none was pending. |
label | <number>, <label-id> | Attach a label (from label list below) to this merge request. Idempotent. |
unlabel | <number>, <label-id> | Detach a label. A no-op if it wasn't attached. |
swarmfile mr create --title "Warmer facade material"
swarmfile mr create --title "WIP: new roof profile" --draft
swarmfile mr ready 4
swarmfile mr list
swarmfile mr list --label <label-id> --assigned-to <principal-id>
swarmfile mr status 3
swarmfile mr diff 3
swarmfile mr approve 3
swarmfile mr request-changes 3 --note "needs another pass"
swarmfile mr merge 3
swarmfile mr close 3
swarmfile mr reopen 3
swarmfile mr comment 3 --message "Looks good once the fascia texture is swapped."
swarmfile mr assign 3 <principal-id>
swarmfile mr request-review 3 <principal-id>
swarmfile mr label 3 <label-id>
approve/request-changes/merge/close put every review action a terminal command away. The hub refuses a review from the merge request's own creator; an approval from a second credential counts, so treat review-capable credentials like any other approval authority in your org. branch protect --required-approvals raises the bar for a unilateral change.
label#
A project's label registry - the tags mr label/mr list --label above attach to and filter by. See assignees, requested reviewers, and labels.
| Subcommand | Flags | Description |
|---|---|---|
list | - | This mount's project's label registry. |
create | <name>, --color <#RRGGBB> | Define a new label. Org admin/owner only. |
update | <label-id>, --name <name>, --color <#RRGGBB> | Rename and/or recolor a label in place - the alternative to delete-and-recreate, which would drop every existing merge request's attachment. Both flags optional; an omitted one is left unchanged. Org admin/owner only. |
delete | <label-id> | Delete a label - cascades off every merge request it was attached to. Org admin/owner only. |
swarmfile label list
swarmfile label create bug --color "#FF5733"
swarmfile label update <label-id> --name defect --color "#000000"
swarmfile label delete <label-id>
rfi#
RFIs and Submittals - the AEC request-for-information / approval workflow, the same feature the dashboard's RFI panel drives. Every command here (other than create/list/search/by-file) takes an RFI, and you can name it either way: the friendly display number (RFI-003, case-insensitive - resolved through the mounted project) or its internal id from rfi create's or rfi list's output. Resolving a number needs a mounted project and the same read access the detail view has.
| Subcommand | Flags | Description |
|---|---|---|
create <path> | --title, --body (required); --priority <low|normal|high|urgent>; --due-at <unix-s>; --assigned-to <id>; --reviewer <id> (repeatable); --kind <rfi|submittal> (default rfi); --draft | Open an RFI (or, --kind submittal, a Submittal) rooted at a mount-relative folder - same path resolution as comment/watch. Needs write there. --draft creates it unsubmitted; call submit when ready. |
list | --status, --priority, --ball-in-court <originator|reviewer|both>, --kind, --assigned-to, --due-before <unix-s>, --outstanding, --limit, --offset | RFIs/Submittals matching the filters. --outstanding narrows to the same still-needs-attention set (excludes closed/void/draft) the file-browser's badge uses. |
status <id> | - | Full detail, including decision history. Exit 0 if answered/closed (someone has responded), exit 2 for every other state (draft/open/in_review/void) - a CI/pre-merge gate can check "has this been answered." |
update <id> | --title, --body, --priority, --due-at, --assigned-to, --ball-in-court | Change mutable fields - every flag optional, only the ones given change. Originator-or-owner only. |
submit <id> | - | draft → open. Originator-or-owner only. |
acknowledge <id> | - | open → in_review - a reviewer marking they've seen it. Assigned-reviewers only. |
close <id> | - | answered → closed. Originator-or-owner only. |
void <id> | - | any → void - abandon it (terminal, leaves the number gap). Originator-or-owner only. |
decide <id> <decision> | -m, --message <text> (required); --signature-kind <click|drawn>; --signature-blob; --comment-id | Record a decision - an RFI's answer, or a Submittal's review outcome. decision is one of approved, rejected, revisions_requested, answered, commented, approved_rev_a, approved_rev_b, approved_as_noted, rejected_as_noted - the last four are Submittal-only; the hub doesn't itself reject a mismatched kind, so check status <id>'s kind first. Comment-or-above ACL. |
decisions <id> | --limit, --offset | The decision audit trail, oldest first. |
assign <id> | --add <principal-id[:role]> (repeatable), --remove <principal-id> (repeatable) | Add/update or remove reviewer-role assignments. role is reviewer (default)/observer/originator. Admin-gated. |
attachments <id> | - | Files attached under the RFI's root. |
search <query> | --project-id, --limit | FTS5 search over title/body, ACL-filtered to what you can read. Defaults to the mounted project; pass --project-id to search a project this engine isn't mounted on. |
by-file <path> | - | Resolve the RFI (if any) that owns a file - the CLI equivalent of the dashboard's right-click "Open RFI thread." |
swarmfile rfi create ./Projects/tower-a/foundations --title "Slab thickness at grid C4" --body "Drawing S-201 shows 300mm, spec calls for 350mm - which governs?" --priority high --reviewer user_88
swarmfile rfi list --outstanding
swarmfile rfi status rfi_9f2a
swarmfile rfi decide rfi_9f2a answered -m "350mm per spec - drawing will be revised"
swarmfile rfi status rfi_9f2a # now exits 0
ec-placement#
Deliberate erasure-coded shard placement across LAN peers.
| Subcommand | Description |
|---|---|
status | Whether this mount currently has deliberate LAN shard placement on. |
enable | Turn on deliberate LAN shard placement. |
disable | Turn off deliberate LAN shard placement. |
ec-placement enable/disablerestart the engine to take effect.
Placement spreads shards across your LAN peers, which are normally found by mDNS. If your network blocks mDNS multicast (common on corporate/VLAN'd networks), the office won't be discovered and every shard falls back to this machine - set lan_from_office (see Engine Config File) so same-office peers count as LAN.
shards <path>#
Where a file's erasure-coded shards live across the office - for each piece, the machines it's assigned to and whether this one holds it. Requires ec-placement enable and a live P2P fabric. The command opens with a per-file summary: how many chunks and shards the file has, whether this machine holds enough shards (k per chunk) to reconstruct it alone, and whether placement actually assigned every shard to K peers - so "is this file protected in the office, and from where" is one line instead of counting rows.
Day-to-day#
status#
This mount's engine, peers, and settings at a glance. It also prints the mounted branch's head commit pin when one is known (head commit: <12-char> (<full hash>)) so a script can capture the exact commit this workspace is on.
pending uploads counts this mount's project. When another project still has saves uploading (one you switched away from keeps uploading in the background), the line adds (N more in other projects) and one indented line per such project follows, named when the engine has seen the project's name and by id otherwise, with any saves held there by a refusal. --json carries the same as pendingUploadsAllProjects and pendingUploadsByProject.
--changes swaps that for a Git-status-like summary of what's outstanding on this mount: the current changelist's staged files and any blocked conflicts. There's no "modified but unsaved" state to show - saves either stage into the changelist or commit immediately - so "staged" plus "conflicted" is the whole picture.
swarmfile status # engine, peers, settings
swarmfile status --changes # staged files + conflicts
stats#
Where this machine's read bytes actually came from - per file and in total: this computer, a colleague's computer, or the cloud - plus first-read timings. The engine counts each block once per file per counter window, at the moment that file is first served it (a cache hit is "this computer"; a LAN/WAN peer transfer is "colleague"; a fetch from cloud storage is "cloud"), including read-ahead, so the cloud and colleague figures are the bytes really downloaded and re-reading or scrubbing a timeline never inflates them. Counters are per project and per engine process; --reset starts a fresh window (use it between takes so each recording's numbers stand alone - it zeroes both this project's breakdown and the engine's process-wide bandwidth counters), and the tray's Data sources panel shows the same counters live with its own reset button.
--json prints the full evidence report - engine version, machine, OS/arch, the mount path/branch, file sizes, per-file source split, read counts and first-read latency - one object you can paste into an evidence log or a journalist can reproduce against. --csv prints the same as spreadsheet rows (a header, a summary row and one row per file, each carrying date/version/machine/project) for the evidence log.
swarmfile stats # human table
swarmfile stats --json # the evidence report
swarmfile stats --csv # spreadsheet rows, one take per machine
swarmfile stats --reset # zero the counters after printing
demo reset#
swarmfile demo reset [--project <id>] [--force] [--yes] - start a demo take cold on this machine: empty the project's local cache (the same whole-project "Free up space" sweep, including "available offline" marks) and zero the stats counters - this project's window and the process-wide bandwidth counters, so every surface the recording shows starts at zero. Run it on every machine a recording uses; a second take that quietly read from the cache is the failure this exists to prevent.
Files with saves that have not committed yet are refused (409) - their bytes may be the only copy - unless -f/--force, which proceeds while still keeping them. Cached content re-downloads on demand and nothing is deleted at the hub. The confirmation prompt is skipped with -y/--yes.
swarmfile demo reset --yes
swarmfile demo reset --project prj_… --force
demo offline#
swarmfile demo offline on [--include-lan] [--for <duration>], swarmfile demo offline off, swarmfile demo offline status [--json] - cut this machine's network for real, on cue, for a recording of "the internet drops mid-work". It installs host firewall rules (macOS pf, Linux nftables or iptables, Windows Firewall), so the engine sees exactly what a dead office uplink looks like; it is not a simulation, and it works with no engine running.
- The office LAN stays up by default. Everything is blocked except this machine itself, private addresses (10/8, 172.16/12, 192.168/16), link-local, IPv6 unique-local and multicast - so office computers keep serving each other while the cloud is out of reach.
--include-lanblocks the LAN too (everything but this machine); a remote shell to it drops. - Existing connections are cut too, including the engine's long-lived connection to the hub - not just new ones.
- It always switches itself back off.
onarms an automaticoffafter 30 minutes (--for 90s,--for 10m,--for 1h30m; at most 24h) that survives the command, the terminal, and on Windows a restart.offends it sooner and cancels the timer; running it when nothing is on is harmless. - Needs administrator rights for
onandoff: on macOS and Linux run it withsudo; on Windows, from an elevated terminal. Without them it exits2and prints the exact command to run.statusneeds none;--jsonreportsoffline,scope,lanStaysUp,since,restoreAtandremainingSecsfor scripts.
sudo swarmfile demo offline on --for 10m # internet off, office LAN up
swarmfile demo offline status
sudo swarmfile demo offline off # back online
op#
Some commands do work proportional to your data - merge, commit, checkout, revert, materialize, branch create/archive, mr merge, trash restore/purge, project delete, ignore-clean, hydrate free-space, and opening or closing a mount. They run as operations in the engine: the command waits and prints the result as usual, however long that takes, but the work no longer depends on the terminal staying open.
- Ctrl-C detaches instead of stopping anything: the command exits
3and prints the operation's id, and the work carries on. --no-waitreturns the id at once (exit3).- An operation's outcome is kept, so you can collect it later - even after the engine restarts. One still running when the engine stopped is reported as interrupted.
- When an operation follows a hub background job - an over-cap folder delete/restore/purge or
project delete(see Trash, History & Rollback) -op statusalso shows itsdone/totalprogress while it runs.
| Subcommand | Description |
|---|---|
op list | Recent operations, newest first. |
op status <id> | One operation's state and, once finished, its result. Exits 0 succeeded, 1 failed, 2 canceled, 3 still running. |
op wait <id> | Wait for it to finish and print its result; exits the way the original command would have. |
op cancel <id> | Stop an operation that can safely stop part-way (materialize, hydrate free-space, git backfill-oids, git index, and checkout/revert while still staging). A cancel that arrives once the staged apply has begun is declined (too_late) and the operation ends with the commit's real outcome. Commits, merges and deletes complete or fail on their own and refuse (not_cancellable). |
swarmfile --no-wait merge --from feature-x # prints the operation id, exit 3
swarmfile op wait 1b2c… # the merge's own result, when it lands
mounts#
Manage this engine process's concurrent mount points. One engine can hold several full mounts open at once - of the same project, or (same org only) a genuinely different one via --project - each with its own changelists, metadata, and lock manager - for running multiple isolated mounts without paying git's one-worktree-per-branch tax (the motivating case: several AI coding agents, each working against the same project without stepping on each other's saves). Most installs only ever run the one boot mount; this is for when you deliberately want more than one. Multiple live mounts are supported on every platform, with a bounded number per machine (18 on macOS): an open fails with mount_failed once that limit is reached - close a mount to free a slot. For the headless, per-agent recipe (API key, labeled mounts, shared cache), see Coding Agents.
| Subcommand | Flags | Description |
|---|---|---|
list | - | Every mount this engine currently has open, (current) marking the one id-less commands (and any command run without --mount) target. |
open <mount_point> | --label <text>, --set-current, --branch <name>, --project <id>, --exclusive | Open a new mount at a filesystem path, a drive letter (Windows: P:), or auto - the next free drive letter on Windows, or a new folder beside the boot mount on macOS and Linux, named after the label or branch. On Windows a folder must be empty or not exist yet (Windows creates the mount folder itself); a folder with anything in it is refused with mount_point_not_empty, and a drive letter something else holds with drive_letter_in_use, naming what holds it. The command returns once the drive has attached, or with the reason it couldn't (mount_failed) - a mount that fails to attach is not left registered. It is a full, symmetric mount, equally capable to the boot mount, not a lightweight second view of it. Does not become current unless --set-current is given, so opening one never retargets a command already running elsewhere against the existing mount. --branch is optional; omit it to mount the project's currently-active branch - but a mount opened without --branch shares the engine's boot branch context and can't be switched independently (mounts branch refuses it with shared_branch_context). Naming a different branch opens a divergent (project, branch) scope: the mount works that branch while every other mount stays on its own, and it can be switched later with mounts branch. --project <id> opens a mount on a DIFFERENT project than this engine's active one (same org only); it combines with --branch, and a cross-project mount pinned to a branch gets that branch's own live-update feed. --exclusive opts the mount into exclusive-write mode: for as long as a file is open for writing through it, a sibling mount opened with the same flag on the same project+branch has its write-open refused (the app sees EAGAIN, the same refusal another machine's lock produces) - a rename, delete, or atomic-save replace of that file is refused too - instead of both editing the file and letting whichever closes last win. A mount that does not set the flag neither takes nor honors claims, so existing multi-mount setups are unchanged; open every agent mount with it when several agents share one tree. The flag is fixed for the mount's lifetime and shown by mounts list/mounts status as exclusive. |
close <id> | --force | Close a mount. Refused (exit 2, changelists_open) if it has open changelists, unless --force is given - never auto-cancels staged work. Refused (exit 2, is_default_mount) for the boot/default mount specifically: closing it shuts down the whole engine, not just one mount, so it isn't reachable through this command at all - quit the engine itself (the Desktop App's Quit, or stopping the swarmfile-engine process) if that's what you actually want. |
current <id> | - | Switch which mount id-less commands target, from then on. |
branch <id> [branch] | --wait | Switch one mount's branch independently of every other mount (omitted = the project's default branch). Only works on a mount that was opened with its own --branch; a mount sharing the engine's boot branch context is refused with shared_branch_context. --wait blocks until the (asynchronous) switch lands or fails. |
status <id> | - | One mount's own detail - the same fields list shows, for just this id, without scanning the whole list. |
scratch-dir <id> | - | That mount's project's machine-local scratch/dependency-cache directory, plus whatever tool-cache redirects its .swarmfile/cache.yml declares (see scratch below). Creates the directory - and every declared subdirectory - if it doesn't already exist. |
The global
--mount <id>flag (see "Global flags" above) is how every OTHER subcommand targets a specific mount instead ofcurrent- e.g.swarmfile --mount mount-abc123 statusrunsstatusagainst that mount regardless of which one is current. It has no effect onmountssubcommands themselves, which already take the id as their own argument, nor onworkspace switch(below), which retargets the whole engine's active workspace; combining the flag with either prints a warning and is otherwise a no-op for the flag.
swarmfile mounts open ~/mount-2 --label "Agent 2" --set-current
swarmfile mounts open auto --label agent-3 --branch feature-x # Windows: next free drive letter
swarmfile mounts open ~/agent-3 --exclusive # exclusive-write mode for agent fleets
swarmfile mounts open ~/src-only --sparse "src/**" # a sparse mount - only src/ is visible
swarmfile mounts open ~/no-vendor --exclude "vendor/**" # everything except vendor/
swarmfile mounts open ~/other-project --project proj_abc123 # different project, same org
swarmfile mounts list
swarmfile mounts status mount-abc123
swarmfile mounts current mount-abc123
swarmfile --mount mount-abc123 status # target it without switching current
swarmfile mounts close mount-abc123
Sparse mount. mounts open --sparse <glob> (repeatable) exposes only the paths matching those include globs - the same dialect as materialize --sparse (a glob matching a directory includes its subtree, a leading ! excludes an earlier match, a bare name matches at any depth). --exclude <glob> is sugar for the ! negations; with no --sparse it means "everything except". An excluded path is invisible at that drive: no listing entry, an open fails as if it weren't there, and a write-create/mkdir/rename onto it is refused. Empty = the whole branch, and the scope is per mount (a sibling mount is unaffected); a scoped mount's offline (hydrate/Pack & Go) set matches the scope.
workspace#
Show or change the org/project this engine is scoped to - the CLI equivalent of the Desktop App's org and project pickers. A same-org project switch usually takes the hot path (no restart), and an org switch does too while the mount is live; switching to a project the running mount can't be re-scoped to, or switching with no live mount to work with, drains and restarts the engine, so the command may briefly drop its connection. A target project whose key hasn't been released yet doesn't force that restart - the switch waits for the grant and completes in place. A same-org switch with creates still queued can also take that restart fallback - queued creates survive it and land in the project they were created under. Refused (exit 2) while a changelist is open, or while another switch is already running.
| Subcommand | Flags | Description |
|---|---|---|
status | - | The current org and active project for this mount. |
list | - | Every org this account can reach, plus every project in the current org, with (active) marking the current pair. Use these ids with switch. |
switch | --org <id>, --project <id>, -y/--yes (--force), --park | Move the engine to that org, and optionally that project in it. --org is required; omitting --project clears the stored project so the engine adopts the target org's default. A switch is refused (exit 2) while a changelist is open - --park sets the mount's open changelists aside first so the switch can proceed; --yes skips the confirmation prompt (which names staged files and queued/failed writes). |
swarmfile workspace status
swarmfile workspace list
swarmfile workspace switch --org org_abc --project proj_9f2a
swarmfile workspace switch --org org_abc # adopt that org's default project
workspace switchchanges the whole engine's active workspace, not one mount, so the global--mountflag has no effect on it (the CLI prints a note if you combine them).
conflicts#
Lists files that are blocked because the same file was changed in two places and the two versions haven't been reconciled yet, and resolves them.
While a file is in that state, writes to it are refused, and an application sees a plain I/O error - a filesystem has no way to explain a cross-machine change. This command is where the reason lives.
| Subcommand | Flags | Description |
|---|---|---|
list (default - bare conflicts is the same) | - | Files blocked by an unresolved conflict, by path. A row on another branch (a branch-merge conflict is recorded under the merge TARGET's entry id) prints the branch name, or (not on this mount's branch) when the hub couldn't name it. |
resolve <path> | --winner local|remote|keep-both | Accept one side non-interactively, without running any tool. |
resolve <path> | --tool [name] | Resolve via an external mergetool speaking git's own mergetool protocol. Mutually exclusive with --winner and --auto. |
resolve <path> | --auto | Run the engine's three-way diff3 auto-merge and accept the result if it's clean - no external tool, no gitconfig needed. Exits 2 when a real conflict remains (a text collision, or a binary). |
resolve --all [--auto | --winner S] | - | Attempt every conflicted file on this mount's branch, printing one summary at the end. Requires --auto (engine merge) or --winner <local|remote|keep-both> (accept that side everywhere). Rows on other branches are skipped and counted unless --include-other-branches is given - pass it when landing a merge (the conflict is recorded under the target branch), or use merge --diff3. Exits 2 if any file still needs a human (auto only), 1 if any request failed. |
resolve --all --include-other-branches | - | With --all, also act on conflicts the hub reports for other branches. |
$ swarmfile conflicts
1 file(s) blocked by an unresolved conflict:
/Projects/plan.dwg
Writes to these files are refused until the conflict is resolved.
Resolve with `swarmfile conflicts resolve <path> --auto`, `--winner
local|remote|keep-both`, or `--tool`, in the Desktop App, or in the web dashboard.
--winner picks a side directly - same three outcomes as the Desktop App/web picker (keep mine, keep theirs, keep both), just from the CLI:
swarmfile conflicts resolve /Projects/plan.dwg --winner local
For a conflict recorded by merge --record-conflicts, local is the incoming branch's version and remote is the version already on the branch being merged into - see Branches & Merging.
When --winner remote keeps a version different from yours, your edit is preserved in the file's history (it shows in blame and the History views) and the command prints a note saying so; swarmfile rollback --at <id> can bring it back.
--auto runs the engine's own three-way diff3 merge and submits it when it comes back clean. It needs nothing installed and no gitconfig, which makes it the resolution path to reach for from a pre-flight or CI script:
swarmfile conflicts resolve /Projects/plan.dwg --auto
It exits 2 (not 1) when auto-merge can't produce a clean result - a genuine three-way text collision, or a binary / non-diff3-eligible file - so a script can tell "a human must choose" apart from "the engine was unreachable" and fall back to --winner or --tool.
Add --all to sweep every conflicted file in the project in one pass and clear the text ones automatically, leaving only the files that genuinely need a person:
swarmfile conflicts resolve --all --auto
--all also accepts --winner, to accept one side across every conflicted file in one pass (useful when a whole branch should keep yours, or theirs):
swarmfile conflicts resolve --all --winner local
--tool [name] resolves a whole-file conflict - including binary files the Desktop App/web picker's "try automatic merge" can't touch - via an external mergetool speaking git's own protocol: $BASE/$LOCAL/$REMOTE/$MERGED temp-file substitution, same as git mergetool. This is the one resolution path that's CLI-only; it needs a real terminal/GUI session to run the tool in, which only the foreground CLI process has.
Resolution order is: swarmfile's own mergetool_cmd config (see the config reference) if you've set one - it wins over gitconfig without even reading it - else your git config's [merge] tool + [mergetool "<name>"] cmd, so anyone with git mergetool already configured and no swarmfile-specific override gets it for free. Name one explicitly to bypass both:
swarmfile conflicts resolve /Projects/plan.dwg --tool # configured default
swarmfile conflicts resolve /Projects/plan.dwg --tool kdiff3 # a specific tool
A nonzero exit from the tool aborts before anything uploads - nothing is resolved, and the CLI tells you where the scratch files ($BASE/$LOCAL/$REMOTE/$MERGED) are for a manual look or retry.
throttle#
Bandwidth throttling - applies immediately, no restart.
| Subcommand | Flags | Description |
|---|---|---|
status | - | Whether throttling is on, the rate in effect right now, and why (office or off hours, and that window's percentage of the site link). |
enable | --download-mbps <N>, --upload-mbps <N>, --office-hours-start <HH:MM>, --office-hours-end <HH:MM>, --office-hours-wan-pct <0-100>, --off-hours-wan-pct <0-100> | Turn throttling on. Omit a flag to leave that value at whatever it already resolves to. |
disable | - | Turn throttling off. |
The --office-hours-* / --off-hours-* flags shape a time-of-day schedule: WAN traffic is capped to a percentage of the site link during the office-hours window (--office-hours-start/--office-hours-end, HH:MM local time) and to a different percentage outside it, so background sync can back off while people are working and open up overnight. The window's start and end must be different times: to use one rate all day, set both percentages to the same value. These same values are file-settable as office_hours_start/office_hours_end/office_hours_wan_pct/off_hours_wan_pct (see Engine Config File).
swarmfile throttle enable --office-hours-start 08:00 --office-hours-end 18:00 \
--office-hours-wan-pct 20 --off-hours-wan-pct 80
What the cap covers. Bulk background transfer: uploads, hydrate, and
fetch. A read an application is actually waiting on - opening a file through
the drive - is not throttled, so a cap set to protect the office link can't
turn somebody's file open into a stall. The practical consequence is that a
large hydrate or fetch is bounded by this setting: both are pre-fetches
filling the cache ahead of use, not somebody sitting in front of a spinner.
cache#
Local disk cache size cap - applies immediately, no restart. This is the block cache (the project's own synced content); for machine-local package-manager/build caches shared across mounts, see run and scratch below - a different, unrelated cache.
| Subcommand | Flags | Description |
|---|---|---|
status | - | Current cache cap and usage. |
set | <gib: u64> | Set the local disk cache cap, in GiB. |
reclaim | --dry-run, --yes | Remove the orphaned pre-project-scoping flat block cache (its blocks re-download on demand). --dry-run reports without deleting; otherwise it asks first (--yes skips). |
resync | - | Re-fetch this project's hub change feed from the beginning - a repair for a mount whose feed cursor advanced past a change it never applied (an entry missing on that machine alone while the hub serves it correctly). Applies are idempotent; local rows and cached bytes are kept, and the walk runs in the background with the engine retrying until it catches up. |
run#
Run a command with this project's declared tool caches (see .swarmfile/cache.yml, under scratch below) redirected into environment variables - e.g. swarmfile run -- pnpm install points pnpm's store at a directory every mount of this project, on this machine, shares, so a second mount (or a second AI agent's mount) doesn't pay to re-download what the first one already fetched. Put -- before the command so its own flags aren't parsed as swarmfile's.
Always safe to prefix onto anything: with no .swarmfile/cache.yml, a project that declares none, a config with a bad entry, or even an unreachable engine, run just warns on stderr and runs the command completely unmodified - it never refuses to run the command over a cache-resolution problem. The wrapped command's stdio and exit code are passed straight through. For wiring several agent mounts onto one shared cache, see Coding Agents.
swarmfile run -- pnpm install
swarmfile run -- cargo build --release
scratch#
This machine's per-project scratch/dependency caches - the machine-local, non-versioned directories run (above) redirects tool caches into. One directory per project, shared by every mount of that project on this machine regardless of branch, the same way a developer's real ~/.cargo/registry isn't per-checkout either. The worked example (agent mounts sharing one cache, with gc) is in Coding Agents.
| Subcommand | Flags | Description |
|---|---|---|
list | - | Every project's scratch cache on this machine: size on disk, how long since it was last used, and whether that project is currently open in this engine. |
clear <name> | --force | Delete ONE project's scratch cache outright, by the name list shows. Refuses if that project is currently open, unless --force. |
gc | --older-than-days <N> (default 30), --dry-run, --yes | Delete every scratch cache that's both closed and untouched for at least the given number of days. Reports what it removed - or, with --dry-run, what it would remove - and the total space reclaimed. Asks first (--yes skips; --dry-run never asks). |
init | - | Write a starter .swarmfile/cache.yml into this mount's project root. Refuses to overwrite an existing file. |
swarmfile scratch init
swarmfile scratch list
swarmfile scratch clear <name> --force
swarmfile scratch gc --older-than-days 14 --dry-run
Nothing here is unrecoverable project data: everything under a scratch cache is a re-buildable tool cache (a pnpm store, a cargo registry, ...), never project content, so clearing one only costs a slower next build, not lost work.
.swarmfile/cache.yml#
A versioned, project-root config file - commit it alongside the project, like .swarmfileignore - declaring which tool caches run should redirect:
caches:
- name: pnpm
env: PNPM_HOME
subdir: pnpm
- name: cargo-registry
env: CARGO_HOME
subdir: cargo
Each entry needs a name (used only in messages), the env variable the tool reads for its cache location, and a subdir (relative, no ..) under the shared scratch directory. A single bad entry - an unsafe subdir, a name collision with an earlier entry, or one of a handful of env vars this refuses to redirect on principle (PATH, HOME, LD_LIBRARY_PATH, and similar) - is dropped with a warning; it never takes the other, otherwise-valid entries down with it. Run swarmfile scratch init if you'd rather start from a commented example than write one by hand.
Only redirect READ-MOSTLY, CONTENT-ADDRESSED download caches here - a pnpm store,
~/.cargo/registry, a pip wheel cache. Not build output directories (target/, an in-progressnode_modulesinstall): sharing one of those across concurrent mounts risks lock contention, not just staleness.
changeset-idle#
Changeset session-minting idle timeout - how long the engine waits after your last write before sealing the open changeset and starting a fresh one on the next write. This groups a burst of related saves into one commit rather than one commit per save. Only applies on a single-project mount. Distinct from changesets list (the commit history) - this is the settings command, not a log.
| Subcommand | Flags | Description |
|---|---|---|
status | - | This mount's current changeset idle timeout, in seconds. |
set | <idle_secs: u64> | Set the idle timeout. 0 disables commit grouping (writes land ungrouped). |
changeset-idle setrestarts the engine to take effect (the changeset manager is only constructed at startup). Persists to the config file'schangeset_idle_secs- see Engine Config File.
attr-cache#
How long (in seconds) the mount may trust cached file/directory attributes before revalidating - this controls how quickly a teammate's newly-saved file stops showing a stale directory listing. 0 means "leave the platform's own default alone" (not "disabled").
| Subcommand | Flags | Description |
|---|---|---|
status | - | This mount's current attribute-cache timeout, in seconds. |
set | <attr_cache_secs: u64> | Set the timeout. 0 keeps the platform default. |
attr-cache setrestarts the engine to take effect, and applies only to macOS (fuse-t) / Linux (vfs) builds - it has no effect on a Windows/winfspmount. Persists to the config file'sattr_cache_secs.
hydrate#
Bulk-materialize a scope of this mount into the local cache, pinned against
eviction until released - for a machine that needs to read that scope with
no network, or that wants everything local before a job starts rather than
fetching lazily during it. It does not reserve anything for writing: to also
keep a scope editable with no hub, use offline prepare, which
reserves the whole scope.
| Subcommand | Flags | Description |
|---|---|---|
start | [path], --yes, --wait, --sparse <glob>, --exclude <glob> | Download and pin every file under path - relative to the mount, or a full path inside it (default: the whole project on the selected mount's branch - use the global --mount <id> to target another mount). Prints a size estimate and a warning if it exceeds your cache cap, and asks for confirmation, unless --yes. --sparse/--exclude (the same globs as mounts open) narrow it to the matching paths; on a sparse mount the mount's own scope already applies, and a request can't widen it. Polls progress until done, except in --json mode, which returns as soon as the job starts unless --wait is given. |
status | - | Progress of the current (or most recent) hydrate job. |
cancel | - | Cancel a running hydrate job. A no-op if nothing is running. |
release | [path] | Unpin everything hydrated under path on the selected mount's branch, making it evictable again. With no path, unpin everything this machine holds for the project - every branch, since the pin map is per machine and project-wide. |
free-space | [path], --dry-run | Free up space: delete the downloaded copy of everything under path so it's cloud-only on this computer, and stop keeping it offline. Files stay listed and download again when opened. Files with changes that haven't synced yet are skipped. With no path it covers the whole project and also clears older cached versions. --dry-run only reports what would be freed. |
swarmfile hydrate start ./Projects/shots/seqA --yes
swarmfile hydrate start ./src --sparse "src/**" --exclude "src/fixtures/**"
swarmfile hydrate status
swarmfile hydrate release ./Projects/shots/seqA
swarmfile hydrate free-space ./Projects/shots/seqA --dry-run
Pinned blocks are never evicted by the cache's normal LRU sweep, so a large
hydrate can push disk usage past your configured cache cap - hydrate start warns about this up front rather than silently growing past it.
Pins are permanent until you run hydrate release, not time-limited.
sync#
Saves that aren't reaching the cloud on their own - the cloud keeps refusing them, or their upload gave up - and a way to give one up.
| Subcommand | Flags | Description |
|---|---|---|
stuck | - | List stuck saves, one per file: the file's entry id, name and path, why it's stuck (commit_refused or upload_failed) and since when. A new file refused at a protected project's root is held, not lost: ask an owner to grant project-wide write, or create a folder to put it in - the full reason sentence is in swarmfile status and the Desktop App's Why did my save fail? |
discard | <entryId>, --yes | Drop the file's stuck saves and put it back to its cloud version. The unsynced change on this computer is lost, so this asks first unless --yes. |
swarmfile sync stuck
swarmfile sync discard 6f1c…e2 --yes
uploads#
Every live in-flight upload on this mount - your own saves and other machines' - from the hub-synced pending pointers. This is the "what is still on its way" view that the Desktop App's Uploading badge summarizes, across everyone, not just this computer.
| Flag | Description |
|---|---|
--wait | Poll until nothing is uploading, instead of printing once. Useful before a checkout or a mirror in CI, where acting on a file that is still landing would read the old version. |
swarmfile uploads
swarmfile uploads --wait # block until every in-flight upload has landed
swarmfile uploads retract <path> clears one file's pending pointer when the machine that was saving it is gone for good - the case where no upload will ever finish and nobody should keep waiting on it. It needs write access to the file, reads the live pointer first, and clears against that pointer's own manifest: if the upload has re-announced since, the retract is a no-op rather than an interruption, so it can never yank a genuinely live upload out from under its writer.
swarmfile uploads retract ./Projects/reel-04.r3d
A pointer nobody refreshes for 30 minutes stops blocking branch creates, merges from its branch, and copies even without this; retracting is for making that explicit and for the readers that wait on the pointer.
An upload that has stalled (a save this machine keeps failing to land) is a different list - see sync stuck.
materialize#
Write a ref's tree to a plain directory - no mount, no FUSE, so it works
on a CI box with no kernel driver. The ref resolves through the same
vocabulary as show/restore (N/#N, HEAD~N, a branch, a tag, a
changeset id, or a 64-hex commit hash; a prefix like hash: or tag: forces
one kind), and it defaults to the selected mount's branch head.
A commit ref - a number, HEAD/HEAD~N, a tag, a hash, or another branch's
name - writes that commit's tree, exactly as it was committed. Only the
default (or the mount's own branch, e.g. branch:main on a main mount) writes
the branch as the mount sees it right now, including saves that haven't been
committed yet. A commit archived before it had a stored tree can't be
materialized (no_commit_tree); pick a newer commit or a branch.
On a protected project, a member's checkout of a historical ref is filtered by the permissions that were in force at that commit - the hub replays its ACL log at the commit's own time, so files they could read then are written even if they're restricted now, and files they couldn't read then are left out. If the hub has no ACL history for that commit it prints a note that the listing was instead filtered against current permissions - never silently. Owners and service/CI callers (see Runner) are never filtered.
The destination must be new or empty, or a directory this tool already
wrote: it keeps a .swarmfile-materialize.json marker and updates in place,
pruning only paths it wrote before - so files your job created between runs
(build outputs, caches) survive, and it never overwrites or deletes a
directory it doesn't own.
A 64-hex hash is the most reproducible pin (tags can be repointed, branches
move). Executable files (see chmod) are written executable on macOS and
Linux. Exits non-zero if any file failed to materialize.
--sparse <glob> (repeatable) checks out only the paths that match - for a
huge repo where a job needs one subtree. A path is written when it matches a
glob or a directory in its ancestry does; *.md matches by name at any
depth, src/** anchors to the root, and a leading ! excludes a path an
earlier glob included (gitignore-style). Everything else is neither fetched
nor written, and a directory with no matching file isn't created. At most 64
globs, each at most 1024 characters, at least one non-!. --exclude <glob>
is sugar for the ! negations (with no --sparse, "everything except"). Omit
either for a full checkout. The clone --checkout form takes both flags.
swarmfile materialize v1.0 --out "$PWD/src"
swarmfile materialize hash:8f14e45fceea167a5a36dedd4bea2543ed10d0f8f9d8f9d8f9d8f9d8f9d8f9d8 --out ./checkout
swarmfile materialize HEAD~2 --out ./prev
swarmfile materialize hash:$COMMIT --out ./checkout --sparse "src/**" --sparse "*.toml"
See Runner (Headless CI) for the full external-CI recipe, including the marker's ownership rules.
git status#
How ready this project is to be served to git. Files at or above the git-LFS pointer threshold (default 1 MiB) would reach git as LFS pointers, and a pointer needs the file's SHA-256 object id. git status walks the selected mount's branch as the mount sees it and reports how many such files there are, how many still have no recorded id, and the first 50 of those with their sizes. It's the readiness check for turning on git access, which refuses while any counted file is missing an id.
It reports the access state first, before the readiness counts: enabled and frozen (with the conversion version and pointer threshold), not enabled (with the command that turns it on), or not available on an end-to-end-encrypted project. When the hub can't be reached, that line is omitted rather than guessed at.
| Flag | Meaning |
|---|---|
--threshold-bytes <N> | Count files of at least N bytes instead of 1 MiB. |
Files saved through the desktop app (or imported with swarmfile-migrate) on a managed or unencrypted project get an id as they're saved. Files added through the web dashboard's uploader or produced by resolving a conflict get one too. Files with no recorded id show as missing until swarmfile git backfill-oids fills them in. An end-to-end encrypted project never has ids - Swarmfile doesn't store a hash of E2E content - and says so; neither does content encrypted with a static key you supply yourself (SWARMFILE_ENCRYPTION_KEY).
It also reports how many commits on the branch are still unconfirmed (unsigned) - with how many of those are disputed and the oldest one's age - when there are any. A commit becomes servable to git as soon as it's derived, even before it's confirmed: a fetch returns the provisional hash and, from a second machine, confirms it. A teammate's app and an API key used in CI can confirm one the same way. A commit that stays unconfirmed for a long time is still worth a look; the warning never changes the exit code.
Exits 0 when every counted file has an id, 2 when some don't (or on an E2E project, or while the hub doesn't yet know the project's encryption tier). --json returns the counts and paths.
swarmfile git status
swarmfile git status --threshold-bytes 10485760 --json
git backfill-oids#
Records the missing ids git status reports. For each file at or above the threshold that has no id, it reads the file through the desktop app's normal read path (downloading it if it isn't cached), computes its SHA-256, and sends the ids to Swarmfile. It works through the files in batches and prints progress as it goes. Project owners and admins only; an end-to-end encrypted project, or one encrypted with your own SWARMFILE_ENCRYPTION_KEY, never records ids and is refused.
| Flag | Meaning |
|---|---|
--threshold-bytes <N> | Count files of at least N bytes instead of 1 MiB. |
Exits 0 when nothing is left missing, 2 when some files couldn't be read (they're listed) or the project doesn't record ids. Re-running it is safe: files that already have an id are skipped.
swarmfile git backfill-oids
swarmfile git status # now reports every counted file as having an id
git enable#
Turns on git access for the selected mount's project - the same one-way switch as the Git access… dialog in the web dashboard. Project owners and org admins only; needs a running engine with the project mounted. Enabling freezes the rules the project is converted to git with: the conversion version, the size above which a file becomes an LFS pointer, and where history starts. The record is immutable afterwards (changing any rule would change commit ids a clone has already seen), so the command prints exactly what was frozen and on which project.
Because the freeze cannot be undone, the command asks for confirmation first: -y/--yes skips the prompt, and a scripted run (--json, or no interactive terminal) must pass it rather than freeze silently.
| Flag | Meaning |
|---|---|
--threshold-bytes <N> | Freeze the LFS pointer threshold at N bytes instead of 1 MiB. |
--no-backfill | Refuse instead of recording missing object ids first. For scripts that want the fail-fast behavior; interactive runs don't need it. |
-y/--yes | Skip the confirmation prompt. Required with --json and when stdin isn't a terminal. |
It refuses, with the reason, while the project doesn't keep every saved version (turn that on from the project's history settings), when an older branch or tag head can no longer be read, when history is too long to check in one pass, and on an end-to-end-encrypted project - git access isn't available there.
If any at-or-above-threshold file still has no object id, the command records the missing ids itself first (the same work as swarmfile git backfill-oids: it hashes each file's content through the mount, fetching what isn't cached) and retries the enable once. --no-backfill skips that and surfaces the coverage refusal directly.
Exits 0 when the record was frozen, 2 on any refusal (the hub's sentence says which) or when the confirmation is missing, 1 when the hub couldn't be reached.
swarmfile git status # everything has an id? then:
swarmfile git enable
swarmfile git clone acme/website
git clone#
Clone a project with git through the swarmfile:// remote helper - the same as git clone swarmfile://<org>/<project> [path], but it finds the git-remote-swarmfile helper next to this binary even when it isn't on your PATH. <project> is a project id, a bare project name (qualified with your configured organization), or <org>/<project>; [path] is the destination directory (git's own default when omitted). It runs git clone for you, so no running engine is needed. The project must have git access turned on - see Cloning a Project with Git for what a clone contains, how it signs in, how git push works from it, and what isn't supported.
swarmfile git clone acme/website
swarmfile git clone acme/website ./website
git import#
Bring an existing git repository - GitHub, GitLab, a local path - into a new Swarmfile project, or into an existing one with --into. The project is created with git access already on and every version kept, and the source's default branch becomes the project's default branch (so upstream commit ids are preserved exactly, including a history whose default branch is master, trunk or anything else). The source is fetched into a temporary bare mirror with your own git, so the same credentials a normal git clone would use - and a configured git-lfs - apply; each branch and tag is then pushed through the same swarmfile:// helper git clone uses. It runs without a running engine, like git clone.
The repository name from the URL becomes the project name unless --name overrides it. Every branch the git view can hold is imported; one it cannot (submodules, Git-LFS, an octopus merge) is skipped and listed, while the same problem on the source's default branch refuses the import before anything is created - nothing can land without it. --branches narrows which branches are attempted (wildcards; the default branch is always imported) and --no-tags skips tags; --submodules fail/--octopus fail widen the refusal to side branches too. Annotated tags are imported as lightweight tags by default (the git view has no tag objects, so tag messages and signatures are not carried; the report says how many); --annotated-tags skip leaves them out and --annotated-tags fail stops the import before creating anything. Tags pointing at something other than a commit, or living only on a branch this import did not push, are skipped and listed. If anything fails to land, the temporary mirror is kept and the report names the retry: swarmfile git import <url> --into <org>/<project> resumes against what already landed; the project is resolved and its git access verified before the source is fetched, so a typo'd or disabled target is refused up front.
The import records the source it came from, so re-running the same command without --name/--into recognizes the project it made and syncs new upstream branches and commits into it instead of failing on the name. --new forces a fresh project anyway (a second project from one source), and an explicit --name always creates the named project.
The importer refuses, before creating anything, a source whose default branch uses Git LFS (LFS objects are not fetched yet), contains submodules, or has an octopus merge - each is a limit of git access itself. A project-name collision after the source is fetched is refused too; pass --name, or --into the existing project.
| Flag | Description |
|---|---|
--name <NAME> | Project name (default: the repository name from the URL). |
--org <ORG> | Organization slug or id (default: your configured organization). |
--template <ID> | A creation template from the project-creation list (default: the git configuration without its starter .gitignore - an import needs the default branch to start empty, and the repository brings its own files, so a template that seeds files can't be imported into). |
--public | Create a public plaintext project instead of the default private managed one (private projects need a plan with them). |
--into <ORG>/<PROJECT> | Push into an existing project instead of creating one; the project is resolved (before the source is fetched), so an unknown name is refused up front. |
--keep | Keep the temporary mirror after a successful import. |
--annotated-tags <convert|skip|fail> | Annotated-tag handling: convert to lightweight (default), skip, or fail before creating anything. |
--new | Create a fresh project even when this source was already imported (the default without --name/--into is to sync into the project this source made). |
--submodules <skip|fail> | A branch containing submodules: skip (default) lists it and imports the rest, fail refuses the whole import. |
--octopus <skip|fail> | Same for a branch containing an octopus merge. |
--branches <GLOB> | Import only branches matching the wildcard (* matches any characters, / included); repeatable, and the source's default branch is always imported. |
--no-tags | Skip every tag (branches still import). |
Exits 0 when every ref landed or was deliberately skipped (a tag pointing at a blob or tree is listed as skipped, not failed), 2 on a refusal (the sentence names the fix) or when a branch or tag push failed (the report names the --into retry), and 1 on a failure such as the source being unreachable. --json prints the structured report. The creation-only flags (--name, --public, --template) are refused alongside --into, which already names the project.
swarmfile git import https://github.com/carlini/js13k2019-yet-another-doom-clone.git
swarmfile git import [email protected]:acme/site.git --name website --org acme
swarmfile git import ./old-repo --into acme/website # retry a partial import
git url#
Print a project's swarmfile:// URL and exit, for wiring a remote by hand. <project> is a project id, a bare project name (qualified with your configured organization), or <org>/<project>. URL segments are printed ASCII-lowercased and percent-encoded where a character needs it (a display name with a space comes out as %20) - the canonical web-address form, which also repairs a caps-typed UUID (a full swarmfile:// URL is passed through unchanged; the remote helper decodes an escaped segment before resolving it).
git remote add origin "$(swarmfile git url acme/website)"
git force-forward#
The append-only form of git push --force, run inside a clone. A branch's tree can't be rewritten on Swarmfile, but the end state a force push wants - "the branch's tree is my tree" - can be landed as a new commit on top of the current head whose tree is your tip's, with your tip as its second parent. This command fetches the branch (which also snapshots uncommitted drive saves, like any fetch), builds that commit with git's own commit-tree (so its id is derived on this machine), and pushes it through the normal helper path: every commit stays reachable, teammates fast-forward instead of diverging, and your tip's history rides the merged/<sha> side branch the helper creates and archives. Only committed work is landed (a dirty worktree is ignored, like git push), and --dry-run still fetches - the dry run skips the push, not the fetch, so a fetch's usual effects apply.
Your local branch is deliberately not moved - the new commit's id cannot match your tip's - so the command prints the git fetch <remote> && git reset --hard <remote>/<branch> that aligns your clone. Pass a BRANCH (default: the checked-out branch), --message <MESSAGE> for the new commit's message (default force-forward: <your tip's subject>), --remote <REMOTE> (default origin), or --dry-run to run every check and push nothing. When your tip simply continues the remote head along its first parents (an ordinary fast-forward), a normal git push lands the same end state, and the command says so instead of building a graft; the diverged merge shape (merging the fetched branch into your line) does need the graft, and the push refusal for it names this command. When the branch already carries your tree there is nothing to do either, and the command just prints the realignment.
A clone can opt in to the same translation for a plain force push: git config swarmfile.force-forward true. With it on, git push --force (and --force-with-lease, whose lease check still runs client-side against the advertised head) pushes the tip as-is when it is an ordinary fast-forward, else builds the same forward commit; a note names the new id and asks you to git fetch and reset, since git's own bookkeeping points your remote-tracking ref at the tip you sent. Default off - force pushes stay refused.
Exits 0 when the push landed (or there was nothing to do), 2 on a refusal - detached HEAD, an unknown local branch or branch name, a fetch that couldn't find the branch, a push the helper refused - and 1 on a transport failure. A rewritten root commit cannot be landed this way: the replaced history shares no commit with Swarmfile, so the push refuses naming the unrelated root - push that as a new branch instead. --json prints the outcome - pushed, fast-forward, already-at-tip or already-carries-tree - with the commit ids; pushed and already-carries-tree carry align, the realignment command for scripts.
swarmfile git force-forward # this clone's branch
swarmfile git force-forward release -m "keep our tree"
swarmfile git force-forward --dry-run
git config swarmfile.force-forward true # let plain --force use the same path
git index#
Derives commits' Git-ready trees on this machine and hands them to Swarmfile, so a committed version can be served to git directly. For each commit it reads the branch state as of that commit's version, builds the tree objects (directories, symlinks and the large-file pointers, splitting very large directories into segments), uploads anything the hub doesn't already have, and claims the result. A second machine - a teammate's app, a fetch, or an API key used in CI - confirms the same value.
With no argument it works on the selected mount's branch head and every unconfirmed commit behind it (through merges too), back to the nearest commit that is already confirmed or that you claimed yourself. Each commit is derived independently and the claims go oldest first, so a chain someone else pushed is confirmed in one run. A run handles up to 1,000 commits and prints one line per commit. Given a commit's sequence number (#42 or a bare number), it handles just that commit.
The git remote helper also does this by itself for a small unclaimed chain: when a git clone, fetch, push or ls-remote meets a branch head that has never been derived, it derives and claims up to 200 commits inline (a clone that set swarmfile.confirm=false opts out). This command is for longer chains, or a machine without the Desktop App.
Requires a running engine and a managed or unencrypted project. Exits 0 when every claim was accepted (including ones still waiting for a second machine to confirm), 2 when the hub disputed or rejected one (later commits in the run aren't claimed), and 3 when the hub couldn't be reached.
A commit printed as rejected (resolved_against in --json) was already settled to a different value when the hub resolved a dispute over it, so this machine's claim is kept only as evidence; compare the two with git disputes SEQ --all and report both hashes, since it is a client bug unless the project's history changed. If the settlement left the commit provisional, an owner who believes it is wrong can override it with git clear-claim; a settlement that confirmed the commit is final.
A commit printed as provisional (your earlier claim was kept) (code self_disagreement in --json) means this machine derived a different value than its own earlier claim for the same commit. One identity deriving two values for one commit is a client bug, not a dispute: the hub keeps the first claim, records the disagreement as evidence (readable with git disputes SEQ --all), and does not alert owners. Report it with both hashes.
swarmfile git index # the branch head and every unconfirmed commit behind it
swarmfile git index '#42' # one commit
swarmfile git index --json
swarmfile git index --verify-full # ignore the cache; re-derive and check every object
--verify-full re-derives without the incremental/sparse caches and checks each derived object against its content address before uploading - a slow but complete self-check for a suspicious or freshly upgraded machine.
git clear-claim#
Owner/admin only: resets the commit claim at a sequence number on the selected mount's project. Most claims are never cleared by hand: Swarmfile's server (for a pushed commit) or a teammate's fetch confirms a provisional one, and a disputed one is settled by Swarmfile deriving it again on the server, usually within a minute or two. This is the escape hatch for a claimed commit that can't be served: a disputed row (two derivations disagree), a claim whose root tree is gone, or a stranded snapshot-mint head - git push mints the branch's live tree as a snapshot when it has drive saves, and if the push is then refused, that snapshot stays as the provisional head; clearing it also rolls the branch head back to the snapshot's parent, so the next fetch mints afresh from the live tree. A healthy claim - including a member's own - is never discarded: the hub answers cleared: false and says why.
Clearing resets the hub's record of the claim, so it is confirmation-gated: -y/--yes skips the prompt, and a scripted run (--json, or no interactive terminal) must pass it. Dispute evidence is kept, not deleted: an owner or admin can read both values, their trees, parents and claimants before or after a clear with git disputes (add --all once it is cleared), or in the Commit disputes section of the project's Commits page on the web.
| Argument/flag | Meaning |
|---|---|
SEQ | The changeset sequence number to clear. |
-y/--yes | Skip the confirmation prompt. Required with --json and when stdin isn't a terminal. |
Exits 0 when the claim was cleared (an already-confirmed or archived seq prints that instead), 2 on a refusal (not an owner/admin) or when the confirmation is missing, 1 when the hub couldn't be reached.
swarmfile git index '#42' # see what the hub holds first
swarmfile git clear-claim 42 # reset it; a stranded snapshot head rolls back
swarmfile git clear-claim 42 -y
git disputes#
Owner/admin only: lists the commit disputes on the selected mount's project, newest first. A dispute means two derivations of the same commit produced different hashes, so git access can't serve that commit until Swarmfile settles the dispute (usually within a minute or two, by deriving the commit again on the server) or an owner or admin clears its claim. Each block shows the commit's sequence number, when the dispute was recorded, the claim's current status, and both sides: the challenging claim and the one it contradicted, each with its commit hash, tree, parents and claimant. A claimant is a user, an API key, or Swarmfile's server-side derivation.
By default only open disputes are listed: not yet cleared, on a commit that is still disputed or provisional. Resolve one Swarmfile could not settle, or override a settlement that left the commit provisional, with git clear-claim; the evidence is kept, and Swarmfile derives the commit again on its own (unless its own derivation was part of the dispute), as does the next swarmfile git index or a teammate's fetch.
| Argument/flag | Meaning |
|---|---|
SEQ | Only the disputes for this commit sequence number. |
--all | Include cleared and evidence-only disputes. |
--json prints the hub's list unchanged. Exits 0 on success (an empty list included), 2 on a refusal (not an owner/admin), 1 when the hub couldn't be reached.
swarmfile git disputes # open disputes on this project
swarmfile git disputes 42 --all # every record for commit 42, cleared ones included
swarmfile git disputes --json
offline#
Pack & Go: materialize a scope and reserve the whole scope with one
long-lived offline reservation, so it stays editable with the hub
unreachable. hydrate above only makes content readable offline; this is the
form that also makes it writable. It runs as a background job, and the scope
follows the selected mount's branch (target another mount with the global
--mount <id>).
| Subcommand | Flags | Description |
|---|---|---|
prepare | [path], --duration-secs <N>, --offline-days <D>, --no-wait | Materialize and reserve. The lease defaults to 7 days, clamped at 30; a lease is only renewed while online, so --offline-days, when longer than the lease, extends the lease to cover the trip (an explicit shorter --duration-secs is never overridden - it warns instead). Prints how many files the reservation covers. If another machine holds an overlapping reservation or is editing a file inside the scope, the prepare is refused naming the holder and nothing is reserved. Then polls to completion unless --no-wait. |
status | - | Progress of the current (or last) job - interruptedScope names one a restart interrupted - plus what this engine holds right now (file edit locks and folder/project reservations, with an expiring-soon and renewal-failure warning). |
cancel | - | Ask a running prepare to stop - reservations already taken are kept, and the content download this prepare started is stopped too (only that job, never a newer one). |
resume | - | Complete a prepare a restart interrupted; re-reserves the scope idempotently. |
return | [path] | Release reservations: at or under one path, or with no path every reservation this engine holds (per-file locks and scope reservations alike). In-scope saves are given a few seconds to land first; any still syncing are reported. |
swarmfile offline prepare ./Projects/shots/seqA --offline-days 14
swarmfile offline status
swarmfile offline return ./Projects/shots/seqA
fetch#
Pull a byte range of one file into the local cache - including from a version somebody else is still uploading.
On a slow link a large upload takes hours or days, and until it finishes the new version isn't readable at all. This asks the uploading machine to send the part you need first, then downloads it as it lands. What you get back is the time it takes to send that range, not the time it takes to send the file.
| Flag | Description |
|---|---|
--tail <bytes> | Fetch the last N bytes. The common case for media, where the part you need is at the end. Resolved against the in-flight size, which is why it's a flag rather than arithmetic you do yourself - see below. |
--start <bytes> | First byte of the range. Default 0. Conflicts with --tail. |
--len <bytes> | How many bytes from --start. Clamped to the end of the file. Conflicts with --tail. |
--version <which> | newest (default - the in-flight version if there is one, otherwise the committed one), pending (fail unless something is uploading), or head (the committed version only). |
--follow | Track a moving reader instead of fetching a fixed range: keep a buffer ahead of wherever it is and re-target when it seeks. --start becomes the initial position; --len and --tail are rejected together with --follow. |
# The last 200 MB of a file a colleague is still uploading
swarmfile fetch ./Projects/reel-04.r3d --tail 209715200
# A specific window of the committed version
swarmfile fetch ./Projects/site.rvt --start 0 --len 52428800 --version head
The command polls until every block has landed, printing progress as it goes:
$ swarmfile fetch ./Projects/reel-04.r3d --tail 209715200
19/160 blocks, 25.8 MiB fetched (waiting on the upload)
...
fetch complete: 160/160 blocks, 200.0 MiB now local
"waiting on the upload" is the normal state, not a stall. The blocks you asked for may not exist yet - the other machine is still sending them at its uplink speed - so this command can legitimately run for a long time. It gives up only when nothing is arriving and the hub reports that the upload has stopped, and says so rather than hanging.
Why --tail rather than working out the offset yourself: the size to measure
back from is the in-flight version's, and the file's committed size is
still the old version's. A number you compute locally would measure from the
wrong end of the wrong file.
--version pending fails outright when nothing is uploading, rather than
quietly giving you the committed version. That is deliberate: somebody who
asked for the version in progress and silently received yesterday's bytes has
no way to tell.
Fetched blocks are pinned exactly like hydrate, so they aren't evicted by
the cache's LRU sweep before you open the file - and they are released the
same way, with swarmfile hydrate release <path>. One job runs at a time;
starting a second while one is running is refused rather than queued.
You can also do this without the CLI: reading the file normally through the
mount always works, and gets the committed version. fetch is for the case
where the version you want is still on its way. (A mount also streams a file
that has never finished uploading - see the config
reference.)
Following a moving reader#
A fixed range is the right shape for "give me the last 200 MB". It's the wrong
shape for watching a file as it uploads, because a viewer plays forward and
seeks. --follow keeps a buffer ahead of a read position you report as it
moves, and re-targets when it jumps.
| Command | Flags | Description |
|---|---|---|
fetch <path> --follow | --start <bytes>, --version <which> | Start following from --start (default 0). Returns once the job is running; it does not complete on its own. |
fetch-seek | <position: u64> | Report where the reader has got to. The buffer re-targets there immediately. A no-op if nothing is following. |
fetch-status | - | Progress of the current (or last) fetch. The way to watch a --follow job. |
swarmfile fetch ./Projects/reel-04.r3d --follow --version pending
swarmfile fetch-seek 5000000
swarmfile fetch-status
For a follow job, fetch-status reports position, bufferedBytes and
readRateBps rather than a block count. bufferedBytes is the number worth
watching: it counts contiguous bytes from the read position, so it answers
"will this play" - where a block total does not. A hole two blocks ahead stalls
a reader however full the rest of the window is.
Buffer depth is derived from how fast the reader is actually consuming, not
from a fixed byte count: 30 seconds of a 6 Mbit/s proxy and 30 seconds of a
200 Mbit/s master are very different numbers of bytes, and a single figure
would be useless for one of them. Set the seconds with
SWARMFILE_STREAM_BUFFER_SECS (default 30). A seek is not counted as
playback, so scrubbing doesn't inflate the estimate.
A follow job ends when you cancel it, when the upload finishes and the buffer
reaches the end of the file, or after five minutes with no fetch-seek - a
paused viewer reports nothing, and one job runs at a time, so an abandoned
follow would otherwise block every later fetch.
seed#
NAS/seed-node mode.
| Subcommand | Description |
|---|---|
status | Current seed-mode state. |
enable | Turn on seed mode. Requires the Pro plan or above, and is mutually exclusive with Cloud-only mode. |
disable | Turn off seed mode. |
seed enable/disablerestart the engine.
A headless cache is easiest to set up from the dashboard: Settings → Office caches generates the complete
seed.env(scope and credential included) and lists the fleet's liveness and served bytes.seed enableis the toggle for an engine whose scope is already configured.
watch / unwatch / watches#
| Command | Description |
|---|---|
watch <path> | Watch a file or folder. |
unwatch <path> | Stop watching a file or folder. |
watches | Everything you're currently watching in this project. |
lock / unlock / locks#
An explicit, up-to-30-day edit lock - like git lfs lock / Perforce's p4 edit. Not the same as the short-lived, automatic lock a plain write takes (from an app's first write to the file until it closes it); this one you take and release yourself.
| Command | Flags | Description |
|---|---|---|
lock <path> | --duration-secs <N> | Reserve exclusive edit rights on a file, even offline. Defaults to 7 days, clamped to a 30-day maximum. Refused (exit 2) if someone else already holds it. Re-running lock on a file you already hold extends it, and a held lock is auto-renewed every 6 hours while the engine is online. |
unlock <path> | - | Release a lock taken with lock. |
locks | - | Every file currently locked - for editing, or being written somewhere - in this project's current branch. |
swarmfile lock ./Projects/tower-a.rvt
swarmfile lock ./Projects/tower-a.rvt --duration-secs 86400 # 1 day
swarmfile locks
swarmfile unlock ./Projects/tower-a.rvt
If someone else needs the file back before you release it, they can ask via unlock-request below - there's no force-release from another user's side.
chmod#
Mark a file executable or not - the same bit chmod +x sets on the mounted drive, and the one git records as mode 100755 (a script, a build tool) versus 100644. Swarmfile stores only this one bit: there are no separate read/write bits per user.
| Command | Description |
|---|---|
chmod +x <path> | Make a file executable. |
chmod -x <path> | Make it a plain, non-executable file. |
swarmfile chmod +x ./tools/build.sh
swarmfile chmod -x ./docs/notes.txt --json
- Files only: a folder or symlink is refused (
not_a_file). - Works offline. The change shows at once on this machine and is sent when the hub is reachable again; the summary line (or
"queued": trueunder--json) says when that's still pending. - On the macOS and Linux drive,
chmoddoes the same thing: any execute bit (u+x,g+xoro+x) makes the file executable, and none makes it not. The drive shows executable files asrwxr-xr-xand every other file asrw-r--r--. - Windows has no execute bit, so
swarmfile chmodis how you set it there. Editing a file on Windows keeps its bit, and so does an app that saves through a temporary file and renames it over the original.
comment / comments#
| Command | Flags | Description |
|---|---|---|
comment <path> | -m, --message <String> (required) | Post a comment on a file. |
comments <path> | --limit <N>, --offset <N> | List comments on a file, newest first. The hub caps one page at 199; --offset pages through the rest. |
trash#
Soft-deleted entries in this mount's project - the headless equivalent of the Desktop App's Trash panel.
| Subcommand | Flags | Description |
|---|---|---|
list | --limit <N> (default 50), --offset <N> | Your own soft-deleted entries, newest-deleted first. |
restore <entry-id> | --with-nearby | Undelete back to its original location. Refused with name_conflict/parent_deleted (exit 2) if something else has since taken that name or the parent folder is itself gone - either way the response names the conflicting entry so you can resolve it by hand. A folder deleted from the desktop drive is deleted file by file, so restoring it brings back the folder and lists the items deleted inside it just before it (nearbyDeleted under --json); --with-nearby restores those too. |
purge <entry-id> | --preview, --yes | Permanently delete ("Delete Forever") - not recoverable, unlike a normal delete. --preview reports how many descendants a folder would take with it, without removing anything. Asks first; --yes skips the prompt (or refuses on a non-interactive stdin / under --json). |
swarmfile trash list --limit 100
swarmfile trash restore 3f9a2b --with-nearby
swarmfile trash purge 3f9a2b --preview
swarmfile trash purge 3f9a2b --yes
unlock-request#
Ask someone to release a file they have locked - distinct from lock/unlock above (edit locks you hold); this is the "please give it back" workflow for a lock someone else holds, the same feature the Desktop App's request-unlock modal drives.
| Subcommand | Flags | Description |
|---|---|---|
create <path> [-m, --message] | - | Ask the current holder to release path. If it just freed up on its own, the hub reports that directly (noLock: true) instead of creating a request nobody needs to answer. |
list [--status] [--entry-id] [--limit] | --status (default pending) | Unlock requests you're involved in, as requester or holder. |
respond <entry-id> <request-id> --decision <granted|denied> | - | As the current holder, grant or deny a pending request against a file you have locked. Exit 2 if it's already resolved or isn't yours. |
cancel <request-id> | - | As the requester, withdraw a still-pending request. Exit 2 if it's already resolved. |
swarmfile unlock-request create ./Projects/tower-a.rvt -m "need to push a fix before EOD"
swarmfile unlock-request list --status pending
swarmfile unlock-request respond 3f9a2b req_88 --decision granted
presence (alias who)#
Who's currently editing what in this project.
check-ignore <path>#
Report whether a path is excluded from sync by .gitignore/.swarmfileignore, and by which rule - the diagnostic counterpart to ignore-clean. Answers "why isn't this file syncing?" the way git check-ignore -v does.
swarmfile check-ignore ./Projects/node_modules/pkg.js
ignore-clean [path]#
A git rm --cached equivalent: remove already-hub-synced content that matches this machine's own .gitignore/.swarmfileignore sync-exclusion rules (see check-ignore) - for content that synced before the matching rule applied, or from a machine whose ignore rules differ. The hub delete is a soft delete (recoverable from Trash for a while), and a matched directory's whole subtree is removed in one call rather than entry by entry. Prints a size estimate and asks for confirmation first, unless --yes; without --yes a non-interactive stdin or --json refuses (exit 2), and --dry-run prints the estimate without confirming or deleting.
| Flag | Description |
|---|---|
[path] | Mount-relative path to narrow the scope to a folder or file. Omit for the whole project. |
--dry-run | Print the size estimate and exit without deleting anything - the preview of exactly what --yes would remove. |
--yes | Skip the confirmation prompt. |
--force | Cross-machine safety override. Alongside --yes, proceed even if a top-level candidate was last modified by someone other than you within the last 7 days. Without --force, --yes refuses in that case. Interactive mode is never gated by this - a human already sees the same who/when detail and answers the prompt regardless. |
swarmfile ignore-clean
swarmfile ignore-clean ./Projects/old-import --yes
completions <shell>#
Generate a shell completion script. Works without a running engine. Supports bash, zsh, fish, elvish, powershell.
swarmfile completions zsh > ~/.zsh/completions/_swarmfile
doctor#
Connectivity diagnostics - asks the running engine to run its probe suite. For a machine where the engine isn't running, use the standalone swarmfile-doctor binary instead.
log#
The commit log as a compact, colored feed (like git log --oneline), scoped to the branch this mount is on. Each line shows the commit's 12-char hash (the full hash is what hash: refs and CI pins use).
| Argument / Flag | Description |
|---|---|
[REF] | Start the log at a commit and walk back from it. Full ref vocabulary (#N/HEAD~N, a branch, a tag, a changeset id, or a 64-hex hash). A branch or tag lists that branch's history (log feature on a main mount shows feature's commits); a bare number, hash, or changeset id walks the mount's branch. --branch wins when given. |
--limit <N> | Default 20 |
--branch <name> | Log another branch's history (read-only) instead of the mount's. |
tail#
Watch this project's activity live - comments, watched-file changes, lock conflicts. Ctrl+C to stop.
This is the engine's local event bus for the mounted project, not the org-wide Activity feed - for that, use activity (list/export/tail). The global --mount flag has no effect on tail: the stream covers every open mount's activity, not just the current one.
Headless engines#
api-key#
Project-scoped API keys, for an engine that runs where nobody can complete a browser sign-in - a render-farm node, a CI runner, a build box. A machine holding one authenticates without an interactive OIDC session at all.
| Subcommand | Description |
|---|---|
list | Every key for this mount's org. Never shows key material. |
create <name> [--project <id>] | Mint a key. --project defaults to this mount's own project; it takes the project id, not its slug (an id the org doesn't have is refused with project_not_found). |
revoke <id> | Revoke by id (from list). Immediate - the key stops authenticating right away. |
rate-limit <id> [max] [--clear] | Set (or, with --clear, clear) that one key's own request ceiling, in requests per minute. Capped at the plan's per-user maximum - use it to let a render-farm/agent key burst above the plan's flat default. |
Minting and revoking require an admin or owner role in this mount's org. The same mint/list/revoke lives in the dashboard, under API keys.
Minting is capped by the plan's headless-key allowance - Free 1, Starter 2 per seat (min 5), Pro 5 per seat (min 10), plus any committed key packs - and a mint past the cap is refused with keys_cap; revoke an unused key or add a pack under Settings → Billing. Revoking frees the slot immediately, and a downgrade never revokes keys you already have - it only blocks minting new ones past the new allowance.
createprints the raw secret once. The hub never returns it again - copy it before you scroll. With--jsonthe secret is in the response body'skeyfield, which is what makes it scriptable; treat that output as the credential it is.
A key is walled off to exactly one project. It cannot read or write anything else in the org - not even projects the admin who minted it can see - so a compromised farm node is confined to the project it was built for, and revoking a key stops that one machine without touching anybody's own session.
Using one. Set it as SWARMFILE_API_KEY on the machine, and the engine skips sign-in entirely: no browser, no refresh token, no impersonating whoever happened to mint it. If both SWARMFILE_API_KEY and SWARMFILE_OIDC_REFRESH_TOKEN are set the API key wins, and the engine says so in its log rather than choosing quietly - a leftover personal refresh token on a shared machine is the more likely mistake of the two.
swarmfile api-key create farm-node-12 --project proj_9f2a
If the mount covers more than one project, --project is required: there is no single obvious answer, and guessing one would scope a credential to the wrong place.
See Deployment Topologies for how this fits with per-node config files and seed nodes.
project#
Project lifecycle and settings in this mount's org - the headless/scripted equivalent of the Desktop App's project settings.
| Subcommand | Flags | Description |
|---|---|---|
create <name> [--e2e] [--public] [--commit-mode] [--project-type] [--keep-full-history] [--description] [--git-compatible] [--acl-mode] [--hub-only] [--residency] [--protect-default-branch] [--initial-branch] [--option] | --e2e; --public; --commit-mode <auto-commit|staged>; --project-type <revit_worksharing>; --keep-full-history <true|false>; --description <text>; --git-compatible; --acl-mode <open|protected> (Open by default; protected needs acls); --hub-only <true|false>; --residency <eu|us|fedramp|fedramp-high> (pins this project's region at creation - per-project and independent of the org's, fixed thereafter; needs the plan's residency capability and provisioned storage for the region); --protect-default-branch; --initial-branch <name> (the default branch's name - most projects keep main; fixed at creation); --option <key=value> (repeatable - any create option by manifest key, e.g. `template=blank | media |
list | - | Every project in this mount's org, with the currently-active one marked. |
get <project-id> | - | Full settings for one project by id (see list for ids). |
rename [name] [--slug] | - | Rename this mount's project and/or its slug. |
archive / unarchive | - | Soft-archive this mount's project, or reverse it. |
delete [--yes] | --yes | Permanently delete this mount's project and all its files, for everyone in the org. A hub-side soft delete: the project leaves active use immediately and its files, history, git-LFS objects, and stored data are permanently purged 30 days later. Recovery isn't self-serve - contact support before the purge window closes to discuss it (it isn't guaranteed; the 30-day window is fixed, and a shorter per-user trash setting does not shorten it). Prints a warning and asks for confirmation first, unless --yes; refuses outright in --json mode without --yes rather than reading stdin. Consider archive instead if you just want to hide it. |
set-keep-full-history <true|false> | - | Opt this project's history out of retention GC (true, every past version stays revertable forever) or back into it (false). New projects default per the organization's new-project default (keep every saved version unless an owner changed it); create --keep-full-history false is the create-time opt-out, and this flips an existing project either way. |
set-type <revit_worksharing|none> | - | Mark this project as Revit-worksharing (native OS-file-lock enforcement, hub-cross-machine), or clear it with none. |
set-commit-mode <mode> | - | Lock this project to a commit mode - a branch lock overrides this while set. mode is auto-commit, staged, or omitted/unset to clear. |
show [project-id] | --json | Print this project's effective settings and where each value came from - the CLI view of the Desktop App's project settings. Defaults to the active project; --json for scripting. |
set <key=value>… | - | Set one or more settings in a single call. Supported keys: commit-mode, keep-full-history, type, name, slug, description, archived, hub-only (true/false/inherit; false and inherit both mean "inherit the org default" - a project can only add hub-only, never force peer-to-peer over an org hub-only policy), allow-external-collaborators (true/false/inherit; the per-project guest policy, where inherit follows the org's), allow-public-rfi and allow-public-access-requests (true/false; the public project's RFI and access-request toggles, on by default). Refuses an unknown key rather than silently ignoring it. |
rotate-key | - | Rotate this mount's E2E project key: mint a new generation and re-wrap it to every current device plus the org recovery key. Owner/admin and E2E only. Content written before the rotation stays sealed under the old generation and is not re-encrypted, so a device that already unwrapped the old key can still read it. |
rename/archive/unarchive/delete/set/set-keep-full-history/set-type/set-commit-mode/rotate-key are owner-or-org-owner-gated: exit 2 on forbidden or not_found, exit 1 for anything else. rotate-key also exits 2 on a 409 (stale_generation - another device rotated first - or a recipient set that kept changing).
swarmfile project create "Tower Block A"
swarmfile project create "Secret Project" --e2e
swarmfile project create "Public Dataset" --public
swarmfile project create "Lean Project" --keep-full-history false
swarmfile project create "Model" --project-type revit_worksharing --description "Central model"
swarmfile project create "Repo" --git-compatible --acl-mode protected --protect-default-branch
swarmfile project create "EU Dataset" --residency eu --option hub-only=true
swarmfile project create "Repo" --git-compatible
swarmfile project show
swarmfile project set commit-mode=staged keep-full-history=false hub-only=inherit
swarmfile project rename "Tower Block A - Phase 2"
swarmfile project set-keep-full-history true
swarmfile project rotate-key
config#
YOUR personal default commit mode for this mount's project - advisory only. Distinct from project set-commit-mode/branch set-commit-mode above, which are admin locks: those, when set, always win over this personal preference. Also distinct from changeset-idle/attr-cache/etc. above, which are local, per-machine engine-behavior knobs, not a hub-side preference tied to your account.
| Subcommand | Flags | Description |
|---|---|---|
set-commit-mode [mode] | - | Set (or, omitted/unset, clear) your personal default. mode is auto-commit or staged. Refused (exit 2, policy_enforced) if an admin lock already resolves for this project+branch - a preference is never silently set unable to take effect. |
get-commit-mode | - | Shows your personal preference (pref, null when unset) and the effective mode with its source: the response's effectiveMode is what this mount's next save or changelist open will use, and effectiveSource is branch or project for an admin lock (a lock always wins), user_pref when your own set-commit-mode default applies, or unenforced when nothing is set. |
swarmfile config set-commit-mode staged
swarmfile config get-commit-mode
swarmfile config set-commit-mode unset
runner#
Read-only run history for the headless runner-as-CI daemon - see Runner (Headless CI) for the full walkthrough. Jobs execute unsandboxed as the daemon's own local user (see Security). There is no runner create/runner edit here: rules live in .swarmfile/runner.yml on the watched branch, not on the hub, so there's nothing on the hub side for this command to create or edit.
| Subcommand | Flags | Description |
|---|---|---|
runs | --branch <name> | Run history - every branch by default, or one branch with --branch. Each row carries the matched rule's name, the triggering commit, status (running/success/failure/timed_out), exit code, and a truncated log. |
swarmfile runner runs
swarmfile runner runs --branch main
ci#
Hosted CI (.swarmfile/ci.yml workflows that run on Swarmfile's own containers - see the Hosted CI guide). Requires the engine to be signed in: reads, settings/size list, run and cancel need a signed-in member; enable/disable, secrets, variables and size writes are owner/admin on the hub, and hosted CI stays off for a project until an owner enables it. In some organizations hosted CI is unavailable entirely, in which case these commands answer 404 ci_not_available. The workflow dialect is documented in the Hosted CI guide.
| Subcommand | Flags | Description |
|---|---|---|
list | --branch <name>, --limit <n> | Run history, newest first - every branch by default. Rows carry the workflow name, run number, commit, status and a blocked reason when admission refused. |
status | <run-id> | One run plus its jobs: status, size, exit code, billed seconds and metered charge. |
cancel | <run-id> | Ask the hub to cancel a queued/running run. |
rerun | <run-id> | Re-run a finished run as a NEW run at the same branch, commit and changeset - every job starts fresh. The hub re-runs the trigger gates, so an E2E project or an unprotected branch without the opt-in refuses exactly like the original trigger (with the same typed code). |
retry | <job-id> | Retry ONE failed job in place (owner/admin). The job's attempt count bumps, its per-attempt fields reset, and the run reopens and re-finalizes when the job finishes. success/running jobs cannot be retried. |
run | --branch <name>, --input NAME=VALUE | Manually dispatch the branch head's workflow (workflow_dispatch). --input is repeatable for workflows declaring on.manual.inputs; the hub coerces values to the declared type. |
logs | <job-id>, --offset <bytes>, --follow | One job's log chunk on stdout (pipe-friendly); --offset continues where the last chunk ended. --follow keeps reading from the offset cursor (~2 s apart) and stops once the job is terminal and the tail is drained; Ctrl-C stops cleanly. |
settings | - | The project's hosted-CI settings. |
enable | --allow-unprotected | Enable hosted CI for the project. --allow-unprotected is the loud opt-in for branches without effective protection (workflows can reference secrets, so leave it off unless you mean it). |
disable | - | Disable hosted CI for the project. |
secret list | - | Secret names and metadata only - values are write-only and never printed. |
secret set | <name>, --from-env <VAR> | Store a secret value: a hidden prompt when run in a terminal, --from-env <VAR> to read it from that environment variable, or piped stdin for scripts. The value never echoes or lands in shell history. |
secret delete | <name> | Remove a secret. |
variable list | - | Names and values of the non-secret config. |
variable set | <name> <value> | Create or update a variable. |
variable delete | <name> | Remove a variable. |
size list | - | Org-defined custom runner sizes. |
size set | <name>, --vcpu, --memory, --disk | Create or update a custom size (quarter-vCPU steps; the platform enforces the ceilings). |
size delete | <name> | Remove a custom size. |
swarmfile ci list --branch main
swarmfile ci status 018f3c2e-…
swarmfile ci logs 018f3c2e-… --offset 65536 | less
swarmfile ci logs 018f3c2e-… --follow # live until the job ends
swarmfile ci rerun 018f3c2e-… # clone a finished run
swarmfile ci retry 018f3c2e-… # re-queue one failed job
swarmfile ci run --branch main
swarmfile ci secret set NPM_TOKEN # hidden prompt for the value
swarmfile ci secret set NPM_TOKEN --from-env NPM_TOKEN # read it from the environment
swarmfile ci variable set REGION eu
swarmfile ci size set big --vcpu 4 --memory 12 --disk 20
office#
Peer-discovery office grouping - applies immediately, no restart.
| Subcommand | Flags | Description |
|---|---|---|
status | - | This mount's current office_id and whether lan_from_office is on. |
set | <office_id>, --lan-from-office | Set the office-grouping label this mount announces for peer discovery. Pair with --lan-from-office on a network that blocks mDNS multicast - see ec-placement above. |
Seed nodes are the one exception to
--lan-from-office: a seed node joins another peer's placement roster purely by matchingoffice_id, with no flag and no shared network required at all - see Self-Hosted Seed Nodes for using this to give a cloud-hosted render farm deliberate redundancy among its own nodes.
acl#
Folder-ACL project mode and the ACL change log - the headless equivalent of the dashboard's ACL settings panel.
| Subcommand | Flags | Description |
|---|---|---|
mode get | - | Whether this mount's project is open (default-readable) or protected (default-denied, entries need an explicit grant). |
mode set <open|protected> | - | Switch project ACL mode. Admin-or-owner-gated: exit 2 on forbidden/not_found. Switching to protected additionally needs the org's plan to include folder ACLs - exit 2 with plan_restricted (402) if not. |
audit [--entry-id] [--limit] [--offset] | - | The ACL change log - every grant/revoke, project- or entry-scoped. |
entry get <entry-id> | - | The ACL roster on one entry (direct + inherited rows). <entry-id> accepts a mount path too (resolved first). Admin-gated: 403s for a member who is neither an org admin nor an entry admin (exit 2), since the roster reveals who else has access. |
entry set <entry-id> | --principal <id>, --permission <read|comment|comment_upload|write|admin>, --effect <allow|deny> (default allow), --inherit | Add or update a grant. Creating one needs the org's plan to include folder ACLs - exit 2 with plan_restricted (402) on Starter. |
entry remove <entry-id> <principal-id> | - | Revoke a principal's grant on an entry. |
principals [--query] [--limit] [--offset] | - | Look up an ACL principal id - the value entry set --principal and mr assign take. --query searches display name/email case-insensitively; without it, lists the org's principals. One page is capped at 50 rows; --offset pages past it, for a search as well as the full list. |
swarmfile acl mode get
swarmfile acl mode set protected
swarmfile acl audit --entry-id 3f9a2b --limit 50
swarmfile acl principals --query "Priya"
swarmfile acl entry get 3f9a2b
swarmfile acl entry set 3f9a2b --principal u_9f2a --permission write --inherit
swarmfile acl entry remove 3f9a2b u_9f2a
acl entry is the per-entry grant surface the project-wide mode toggle doesn't cover; the same grants are editable from the dashboard's permissions panel, and (on Windows) projected as NTFS DACLs on the mount.
e2e-key#
End-to-end encryption device keys.
| Subcommand | Description |
|---|---|
status | This device's own enrolled key - id and public key - read straight from the local cache file, no hub round trip. enrolled: false means this device hasn't finished enrolling yet (or the binary was built without the e2e feature at all, flagged with e2eBuild: false). |
list | Every device key on this account - every machine, not just this one. For finding the id of a lost device to revoke. |
revoke <id> | Revoke a device key by id (see list) - e.g. after a lost laptop. |
There's no enroll here - a device enrolls its own key automatically at startup.
swarmfile e2e-key status
swarmfile e2e-key list
swarmfile e2e-key revoke key_9f2a
device-transfer#
Move a device's end-to-end project keys onto a second device without going through the org's recovery-key quorum - a one-time, ten-minute code links the new device to an already-enrolled one and moves the wrapped project keys across, entirely client-side. The private key never touches the hub.
| Subcommand | Description |
|---|---|
start | Run on the device that already has the project's key. Prints a one-time code, waits for a new device to link, then uploads this mount's currently-mounted project key to it automatically. Requires an enrolled E2E key and a mounted E2E project on this engine. |
link <code> [--label] | Run on the new device with the code from start. Waits for the other device to upload, then finalizes - re-wrapping every recovered project key to this device's own already-enrolled key. --label names the device (shown later in e2e-key list and Settings → My Devices). |
Both commands poll every couple of seconds until the handshake completes or the code expires; past 10 minutes either side reports a clear "expired, run start again" rather than hanging.
# On the device that already has the project:
swarmfile device-transfer start
# Transfer code: 7F3K-9QRT
# On the new device, within 10 minutes, run:
# swarmfile device-transfer link 7F3K-9QRT
# On the new device:
swarmfile device-transfer link 7F3K-9QRT --label "Jordan's laptop"
start transfers only the mount's currently-configured project - run it again, once per mounted project, on a device that holds more than one. There's no --enroll step on the new device: every engine enrolls its own long-term device key automatically at startup, and link/finalize re-wrap to that key directly.
If nobody's engine is online to complete an ordinary access grant, an org member can also fulfill it manually from the web dashboard rather than running this at all - see End-to-End Encryption Setup.
admin nodes#
The hub's P2P mesh node allow-list - owner-only. node_id is the 64-hex-char Ed25519 public key an engine self-generates (visible in the engine's own startup log, or in a not-yet-authorized node's connection-refused message). The same list, with an authorize form and a revoke button, is also available from Settings → Nodes in the web dashboard.
| Subcommand | Flags | Description |
|---|---|---|
list | - | Every node currently on the org's allow-list. |
authorize <node-id> <user-id> [--label] | - | Add a node, authenticating as user-id. |
revoke <node-id> | - | Remove a node - it can no longer join the mesh. |
swarmfile admin nodes list
swarmfile admin nodes authorize a1b2c3...64hex user_88 --label "render-farm-3"
admin retention#
The organization's trash/history retention window and its size-adaptive floor, plus the history-retention default for NEW projects - owner-only. When a project approaches its storage limit, the hub automatically shrinks the retention window under pressure; the floor is the tightest it may ever shrink to. Both are null (the global defaults) until set. keepFullHistoryDefault decides what newly created projects start with: true (the product default) keeps every saved version, false starts them on named commits only - a creator's explicit choice at creation still wins. The same controls are in Settings → Organization in the web dashboard.
| Subcommand | Flags | Description |
|---|---|---|
get | - | The current window, floor, and new-project retention default. |
set | --days <1-3650>, --floor <1-3650>, --clear-days, --clear-floor, --keep-full-history-default <true|false> | Set and/or clear the day values, and/or set the new-project default. Omitted fields are left unchanged. The hub rejects a floor above the window (it would be inert). |
swarmfile admin retention get
swarmfile admin retention set --days 120 --floor 30
swarmfile admin retention set --clear-floor
swarmfile admin retention set --keep-full-history-default false
admin rate-limits#
The organization's request budgets: the per-org and per-user request ceilings, in requests per minute - owner/admin-configurable. The same limits appear in the tray's Settings → Developer panel and the web dashboard's Settings → Organization page. show reports the current values alongside each plan default and maximum; set changes them. The Free plan's limits are fixed, Starter/Pro cap at their tier maxima, and Enterprise's org ceiling can be removed entirely. A PAT shares its owner's per-user window; an API key gets its own window at the plan's default per-user value; content streaming isn't counted here.
| Subcommand | Flags | Description |
|---|---|---|
show | - | The current per-org and per-user ceilings, their plan defaults and maxima, and whether each is configurable. |
usage | - | This minute's usage against the ceilings, for the whole org and for you (the caller), including the separate media-read windows. |
set | --org <n>, --user <n>, --clear-org, --clear-user, --unlimited-org, --for <dur> | Set and/or clear either ceiling. Omitted fields are left unchanged. --clear-* resets a field to the plan default; --unlimited-org removes the org ceiling entirely (Enterprise only); --for 6h (units s/m/h/d, max 30 days) makes a raise auto-revert. The hub rejects a value above the plan's max and refuses any change on the Free plan. |
rate-limits is also available at the top level (swarmfile rate-limits …) - the same subcommands, without the admin prefix.
swarmfile admin rate-limits show
swarmfile admin rate-limits usage
swarmfile admin rate-limits set --org 4000 --user 450
swarmfile admin rate-limits set --org 8000 --for 6h
swarmfile rate-limits set --clear-org
swarmfile rate-limits set --unlimited-org
notifications#
This account's notification inbox - comments, mentions, unlock requests, merge requests, RFIs, quarantine, failed uploads, and more. list/unread-count/read/read-all hit the hub's full notification feed (the same inbox the dashboard's bell shows), not just this machine's own upload failures.
| Subcommand | Flags | Description |
|---|---|---|
list | --kind, --unread, --since <time>, --limit | Notification history, newest first. --since accepts Unix seconds, epoch milliseconds, a duration (2h, 7d), or an RFC 3339 timestamp. --kind accepts a hub notification kind (the same open vocabulary - comments, mentions, unlocks, RFIs, MRs, quarantine, uploads, storage, …); an unknown value is rejected with the accepted list and exit 2, so a typo can't silently return the whole inbox. |
unread-count | - | Cheap count for a shell prompt or a CI/cron check. |
read <id> | - | Mark one notification read. |
read-all [--kind] | - | Mark every notification read, optionally narrowed to one kind. An unknown --kind is rejected (exit 2) rather than marking the whole inbox read. |
preferences get | - | Current email/webhook delivery preferences - the webhook status (url/enabled/configured) rides along in this same output. |
preferences set | --email-enabled [<true|false>] (bare = on), --quiet-start/--quiet-end (HH:MM), --timezone, --kind <KIND=on|off|default> (repeatable) | Update one or more fields - omitted flags leave the existing value untouched. --quiet-start/--quiet-end must both be set together or both left unset. Clearing quiet hours or the timezone is dashboard-only: the API takes an explicit null for it, but the CLI has no null form (an empty value is a validation refusal), so there is no CLI clear. --kind sets a per-kind email override (off mutes that event, on forces it, default clears that kind's whole override - email and webhook - back to the kind's defaults: the global switches plus that kind's own default, where one applies); the CLI merges into your current overrides, so other kinds and their webhook flags are preserved. An unknown kind or malformed spec exits 2. Exit 2 on a 400 (mismatched pair or malformed time). |
preferences webhook set <url> | --enabled [<true|false>] (bare = on) | Set (or change) YOUR OWN personal webhook - a third notification channel next to the in-app bell and email, one URL per user. The <url> argument is required on every call (no partial "just flip --enabled" shorthand - pass the same URL again to toggle it alone). Mints a fresh signing secret whenever the URL changes, or is set for the first time, and prints it ONCE. Distinct from the org-wide swarmfile webhook command group below: this one is fire-and-forget, with no delivery log or auto-disable. |
preferences webhook clear | - | Remove your personal webhook. |
preferences webhook rotate-secret | - | Mint a fresh signing secret without changing the URL. Hard cutover - the old secret stops verifying immediately. Prints the new secret ONCE. |
preferences webhook test | - | Fire one synthetic event at your configured webhook right now, synchronously. Nothing is logged - this channel has no delivery log - the result prints directly. |
swarmfile notifications list --unread --limit 20
swarmfile notifications list --kind comment_mention
swarmfile notifications read-all
swarmfile notifications preferences set --quiet-start 22:00 --quiet-end 07:00 --timezone America/New_York
swarmfile notifications preferences set --kind file_changed_by_other=off
swarmfile notifications preferences set --kind comment_added=default # clears email + webhook for that kind
share#
Share links - a read/comment/respond link to one file, handed to someone outside the org. This covers only the member-authed management side: creating your own links, listing/revoking them, and (owner-only) the access log and email block-list. The anonymous recipient's own side of a link is the web-hosted /s/:token page, not a CLI surface.
| Subcommand | Flags | Description |
|---|---|---|
create <entry-id> | --expires-at <unix-secs>, --max-accesses <N>, --password <string>, --allow-comments, --allow-responses | Create a link for one file. <entry-id> accepts a mount path too (the path is resolved first). Exactly one of --allow-comments/--allow-responses is meaningful - the hub picks the more permissive one if both are set. --allow-responses is the RFI share-link path (needs a directory entry, not a file) - see RFI attachments. Exit 2 on a 400 (missing/invalid entry-id, or wrong entry type for the requested permission) or a 404 (entry gone). |
list | --entry-id | The caller's own share links, optionally narrowed to one file. |
revoke <id> | - | Revoke a link. Exit 2 on 404 (not found, or not yours). |
access-log <id> | --limit, --offset | Who has actually opened this link (the mandatory-email-gating log, not just a hit count). Owner (creator) only - exit 2 on 404. |
block <id> <email> | - | Block one email from this link going forward. Owner only. Takes effect on that email's next gated request - an already-issued 30-day session cookie isn't separately revoked. |
unblock <id> <email> | - | Unblock a blocked email address. Owner only. |
swarmfile share create e-1a2b3c --allow-comments --expires-at 1735689600
swarmfile share list --entry-id e-1a2b3c
swarmfile share access-log shr_9f2a --limit 20
swarmfile share block shr_9f2a [email protected]
swarmfile share revoke shr_9f2a
activity#
The org/project activity feed - commits, branch switches, comments, CI runs, membership/ACL changes, who did what and when. An org owner sees everything; everyone else only what they have read access to (owner-only sources like quarantine and org-policy events project to empty for them).
| Subcommand | Flags | Description |
|---|---|---|
list | --since/--until <time>, --limit, --kind <csv>, --project-id, --entry-id, --actor-id, --cursor | The feed, newest-filtered-window first. Defaults to the last 7 days when --since/--until are omitted. --since/--until accept Unix seconds, epoch milliseconds, a duration (2h, 7d), or an RFC 3339 timestamp. --kind takes a comma-separated list of event kinds. --cursor resumes from a previous response's pagination cursor. |
export | --since/--until, --kind, --project-id, --out <path> | Unbounded CSV export (capped at 100k rows hub-side) - org-owner-only, and needs the Pro+ audit_log entitlement (exit 2 on the 403 either gate returns). Prints CSV to stdout by default; --out writes it to a file instead. Not paginated - narrow --since/--until if you need a complete window (a response is capped at 100k rows). |
tail | --interval <secs> (default 3), --since <time>, --kind, --project-id, --actor-id | Follow the feed: print new events as they land, until Ctrl+C. Starts from now unless --since is given (same time shapes as list). |
tail follows by polling the same GET /activity endpoint list uses - polling is the supported way to follow the feed. Expect a few seconds of latency, bounded by --interval. With --json it prints one event per line (JSON-lines), so a consumer can process the stream incrementally instead of re-parsing a document that repeats each poll.
swarmfile activity list --kind mr_merged,commit_created --limit 50
swarmfile activity list --since 1735689600 --project-id p_9f2a
swarmfile activity tail --kind commit_created
swarmfile activity export --since 1767225600 --until 1798675200 --out audit-2026.csv
webhook#
Manages this org's outbound webhook subscriptions - signed HTTP callbacks fired on file/branch/merge/ACL/membership events, straight into Slack, Zapier, a SIEM, or your own endpoint. Everything the web dashboard's webhook settings page does is available here too; this is the org-wide subscription list, distinct from the single personal channel under notifications preferences webhook above.
| Subcommand | Flags | Description |
|---|---|---|
list | - | Every webhook subscription for this mount's org. Never shows signing secrets. |
create <url> | --event <kind> (repeatable), --description | Subscribe a URL to org activity events. Prints the raw signing secret ONCE - copy it now, the hub never returns it again. --event may be repeated to subscribe to specific kinds only; omit it to receive every kind. |
delete <id> | - | Remove a subscription. Immediate. |
reenable <id> | - | Clear a subscription's auto-disable - the hub disables one automatically after too many consecutive delivery failures. |
rotate-secret <id> | - | Mint a fresh signing secret. Hard cutover - the old secret stops verifying immediately. Prints the new secret ONCE. |
update <id> | --url, --description, --clear-description | Update a subscription's URL and/or description. An omitted flag keeps its current value. Doesn't touch which event kinds it receives or its custom headers. |
deliveries <id> | --limit, --cursor | A subscription's delivery log, newest first, cursor-paginated (use the previous response's nextCursor). |
test <id> | - | Fire one synthetic event at a subscription's URL right now, synchronously. Recorded in the delivery log, but never counts toward auto-disable health tracking. |
retry-delivery <id> <delivery-id> | - | Manually re-send one past delivery. A delivery with no stored payload can't be retried. |
swarmfile webhook create https://hooks.example.com/swarmfile --event mr_merged --event commit_created
swarmfile webhook list
swarmfile webhook deliveries wh_9f2a --limit 20
swarmfile webhook rotate-secret wh_9f2a
Scripting against the CLI#
Pass the global --json (before or after the subcommand - both work) and it prints the engine's raw response, which is the intended path for automation - commit from CI, hydrate before a render, check conflicts in a pre-flight script (conflicts resolve <path> --auto clears text conflicts headlessly, exiting 2 when a human is needed). A usage error under --json emits {"code":"usage_error",…} rather than clap's prose, so the same parser handles both. Pair it with an API key and no part of the loop needs a human.
For reacting to changes rather than polling for them, tail streams this project's activity as it happens - hold that connection open for sub-second reaction. If your integration doesn't want to hold a connection open at all, swarmfile webhook create (org owner) or notifications preferences webhook set (personal) both set up a signed HTTP callback to a URL you control instead. They differ in delivery: org webhooks are delivered on a periodic schedule, not in real time, with retries and a delivery log; the personal channel is fire-and-forget, enqueued as each notification happens, with no delivery log, retry, or auto-disable.