Boxyard CLI Skill
Use this skill when the user wants to use Boxyard, not develop Boxyard itself.
Boxyard is a Python CLI for managing and syncing folders ("boxes") across local and remote storage using rclone or local storage. A box has data, metadata, optional sync configuration, group membership, and sync records.
Where generated data should live
Generated data — including large outputs — generally belongs inside the box it relates to, not in some sibling directory chosen to dodge syncing. Do not move heavy outputs out of a box to keep it "small". The whole point of Boxyard is that syncing makes large boxes comfortable to live with: anything you don't want pushed can be excluded from sync via the box's conf/.rclone_exclude / conf/.rclone_filters (see "Per-box sync configuration"), or the box itself can be excluded locally with boxyard exclude. Keep data colocated with its box and control sync with filters — don't fragment it to avoid sync.
Before running commands
If operating from this repository checkout, prefer:
cd /path/to/boxyard && uv run boxyard ...If Boxyard is already installed in the environment,
boxyard ...is also fine.Read-only commands are safe to run without confirmation:
--help,list,tree,path,which,box-status,yard-status,list-groups,owner,doctor.Ask before running commands that can modify local or remote state:
init,new,sync,multi-sync,sync-missing-meta,include,exclude,delete,rename,sync-name,add-to-group,remove-from-group,add-parent,remove-parent,create-user-symlinks,copy,force-push,claim,release,discard-local.Be especially careful with:
boxyard new --from PATH/-f PATH: movesPATHinto Boxyard unless--copyis supplied.boxyard exclude: syncs first by default, then removes the local data copy.boxyard delete: deletes a box.boxyard sync --sync-setting replace|force: can overwrite data depending on direction/status.boxyard force-push --force: destructively overwrites remote data from a local source folder.
Configuration files and important paths
Default files and folders:
~/.config/boxyard/config.toml # main Boxyard config
~/.config/boxyard/boxyard_rclone.conf # Boxyard's rclone config
~/.config/boxyard/default.rclone_exclude
~/.boxyard/ # default boxyard_data_path
~/boxes/ # default user_boxes_path; included box data appears here
~/box-groups/ # default user_box_groups_path; group symlinks appear here
The config file controls the real locations. Important config keys:
default_storage_location = "..."
boxyard_data_path = "~/.boxyard"
user_boxes_path = "~/boxes"
user_box_groups_path = "~/box-groups"
[checkout_roots.bulk]
path = "~/large-boxes"
# Optional guarded root (both keys required together)
[checkout_roots.volume]
path = "/mnt/volume/boxes"
mount_target = "/mnt/volume"
filesystem_uuid = "..."
max_concurrent_rclone_ops = 3
[storage_locations.my-remote]
storage_type = "rclone" # or "local"
store_path = "boxyard"
Derived paths:
<boxyard_data_path>/boxyard_meta.json # cached local box index
<boxyard_data_path>/local_store/<storage>/ # local metadata/conf roots by storage location
<boxyard_data_path>/sync_records/ # local sync records
<boxyard_data_path>/sync_backups/ # local sync backups
<boxyard_data_path>/remote_indexes/ # cached remote index lookups
<boxyard_data_path>/placements/<box_id>.json # machine-local checkout placement
For a box with index name <box_id>__<name>:
<configured-checkout-root>/<box_id>__<name>/ # authoritative local DATA path
<boxyard_data_path>/local_store/<storage>/<index>/ # local box root
<boxyard_data_path>/local_store/<storage>/<index>/boxmeta.toml
<boxyard_data_path>/local_store/<storage>/<index>/conf/
Remote/rclone stores use this layout under the storage location's store_path:
boxes/<index>/data/
boxes/<index>/boxmeta.toml
boxes/<index>/conf/
sync_records/<index>/<data|meta|conf>.rec
sync_backups/
The global CLI option for non-default config is:
boxyard --config /path/to/config.toml <command> ...
boxyard init uses --config-path and --data-path to create a config/data directory. The shell helper honors BOXYARD_CONFIG_PATH; normal Typer CLI commands should be given --config when using a non-default config.
DEFAULT_BOX_GROUPS can add default groups at runtime. It is parsed as a TOML list string, for example:
export DEFAULT_BOX_GROUPS='["ctx/mac", "work"]'
How to find where boxes are
Use these patterns first.
Find the box containing the current directory or any path
boxyard which
boxyard which --path /some/path
boxyard which --path /some/path --json
boxyard which --path /some/path --index-name
which searches every configured checkout root (including symlink-resolved paths) and reports the box name, id, index name, storage location, checkout root/state, authoritative local DATA path, and inclusion state.
Get a box's data folder
boxyard path --box-name NAME --pick-first
boxyard path --box-id BOX_ID
boxyard path --box INDEX_NAME
boxyard path defaults to the data path. By default it filters to included boxes. Use --all when you need to select from included and excluded boxes.
Get non-data paths for a box
boxyard path --box-name NAME --pick-first --path-option root
boxyard path --box-name NAME --pick-first --path-option meta
boxyard path --box-name NAME --pick-first --path-option conf
boxyard path --box-name NAME --pick-first --path-option sync-record-data
boxyard path --box-name NAME --pick-first --path-option sync-record-meta
boxyard path --box-name NAME --pick-first --path-option sync-record-conf
Find checkout roots and actual local paths
Never reconstruct user_boxes_path/<index_name>: a box may be in any configured root. user_boxes_path is permanently the root named default; additional roots are [checkout_roots.NAME]. Use:
boxyard checkout-roots
boxyard list --show-status --show-checkout
boxyard list --checkout-root volume --output-format json
boxyard path --box INDEX_NAME
boxyard which --path /some/root/INDEX --json
Status markers are ● included, ○ excluded, ! root unavailable, × recorded checkout missing, and ↔ interrupted relocation. An unavailable root never falls back to default and its boxes remain in the catalog.
Common discovery commands
boxyard list
boxyard list --show-status
boxyard list --output-format json
boxyard list --view groups --show-status
boxyard list --view tree --show-status
boxyard tree --show-status
boxyard list-groups --all --include-virtual
boxyard yard-status
Group filters support boolean expressions over group names:
boxyard list --group-filter 'work AND NOT archived'
boxyard path --group-filter 'ctx/mac OR ctx/linux' --pick-first
Other list filters:
boxyard list --include-group GROUP
boxyard list --exclude-group GROUP
boxyard list --children-of BOX
boxyard list --descendants-of BOX
boxyard list --parent-of BOX
boxyard list --ancestors-of BOX
boxyard list --roots
boxyard list --leaves
Box selection options
Many commands accept one of:
--box INDEX_NAME # full <box_id>__<name>
--box-id BOX_ID # <timestamp>_<subid>
--box-name NAME # defaults to contains matching for many commands
Name matching options:
--name-match-mode exact|contains|subsequence
--name-match-case
--pick-first # available on `path`; use only when ambiguity is acceptable
With no --box/--box-id/--box-name at all, boxyard uses the box you are
standing in — anywhere under <user_boxes_path>/<index_name>/.... If the cwd is
not inside a box (or the box is not a candidate for that command, e.g. an
already-included box for include), it falls back to an fzf picker over the
candidates. Commands that destroy something — delete, rename, copy,
force-push, sync-name — refuse a bare invocation outright and always need
an explicit selector.
Creating boxes
Create an empty box:
boxyard new --box-name NAME
Create from an existing folder, moving the folder into Boxyard:
boxyard new --from /path/to/folder
Copy from an existing folder instead of moving it:
boxyard new --from /path/to/folder --copy
Clone a git repo as a new box:
boxyard new --git-clone git@github.com:user/repo.git
Useful options:
boxyard new --box-name NAME --storage-location STORAGE
boxyard new --box-name NAME --group GROUP --group OTHER_GROUP
boxyard new --box-name NAME --parent PARENT_BOX
boxyard new --box-name NAME --no-initialise-git
Select local placement independently of remote storage:
boxyard new --box-name NAME --storage-location STORAGE --checkout-root ROOT
boxyard include --box-name NAME --checkout-root ROOT
boxyard relocate --box-name NAME --checkout-root OTHER_ROOT
boxyard relocate --box-name NAME --checkout-root ROOT --adopt-existing
boxyard relocate --box-name NAME # recover the recorded destination after interruption
exclude remembers the preferred root; include without a root reuses it. relocate is locked, local-only, does no remote I/O, and is recoverable via doctor. Use --adopt-existing only for a pre-populated destination: Boxyard verifies every source entry is identical there, preserves destination-only content, then commits placement and removes the source.
Syncing
Sync one box:
boxyard sync --box-name NAME
boxyard sync --box INDEX_NAME
boxyard sync --box-id BOX_ID
Sync only selected parts:
boxyard sync --box-name NAME --sync-choices meta
boxyard sync --box-name NAME --sync-choices conf
boxyard sync --box-name NAME --sync-choices data
Sync settings and direction:
boxyard sync --box-name NAME --sync-setting careful
boxyard sync --box-name NAME --sync-setting replace
boxyard sync --box-name NAME --sync-setting force
boxyard sync --box-name NAME --sync-direction push
boxyard sync --box-name NAME --sync-direction pull
Other sync commands:
boxyard multi-sync
boxyard multi-sync --storage-location STORAGE --max-concurrent 3
boxyard multi-sync --box INDEX_NAME --box OTHER_INDEX_NAME
boxyard sync-missing-meta
boxyard box-status --box-name NAME
boxyard yard-status
Soft interruption is enabled by default for long operations: interrupt once or twice to stop after the current operation; repeated interrupts exit immediately.
Write ownership (owner, claim, release, discard-local)
A box can have a write owner: the single machine allowed to push its DATA. A box with
no owner is unrestricted, exactly as before this feature existed — so most boxes are
unowned and nothing about them changed. Ownership is recorded per box as write_owner
and compared against this machine's configured machine_name (configured, never derived
from the hostname, because hostnames are unreliable — one machine reports both
lukas-pocket4 and pocket4).
boxyard owner --box-name NAME # who may push this box (read-only; -o json too)
boxyard claim --box-name NAME # make THIS machine the write owner
boxyard claim --all-included # claim every box included here that has no owner
boxyard release --box-name NAME # give up this machine's ownership
If a sync is refused because another machine owns the box, there are exactly two ways out, and the error prints both:
boxyard claim --steal --box-name NAME # take ownership from the current owner (prompts; -y to skip)
boxyard discard-local --box-name NAME # throw away THIS machine's copy, take the remote's
discard-local is the destructive one, but not lossy: what it overwrites is kept under
the sync backups directory and the path is printed. Prefer --steal when this machine's
copy is the one you want to keep, discard-local when the remote's is.
Ownership is also enforced on three commands that bypass sync entirely and would
otherwise write to the remote unchecked: force-push, rename --scope remote|both, and
delete. Being refused by one of these is the gate working, not a bug — resolve it with
claim/--steal rather than reaching for a workaround.
Health check (doctor)
boxyard doctor is a strictly read-only health check of the machine's whole boxyard state. It never mutates or auto-fixes anything, and exits 0 when healthy / 1 when there is any finding, so scripts and cron jobs can assert on it.
Agents: run boxyard doctor whenever box state looks inconsistent — e.g. a folder in user_boxes_path that boxyard list doesn't know about, boxyard list missing boxes that exist on another machine, group symlinks pointing nowhere, or errors mentioning boxmeta/sync records. Every finding comes with a one-line hint on how to fix it; apply the hints rather than improvising.
boxyard doctor # full check, including remote storage
boxyard doctor --no-remote # offline: skip remote checks (stale-meta-mirror)
boxyard doctor -o json # machine-readable report
boxyard doctor -s STORAGE # restrict the remote check to one storage location
Checks: unregistered-folder (dirs in user_boxes_path not registered as boxes — the classic symptom of hand-creating folders instead of using boxyard new), malformed-name (names that don't parse as <timestamp>_<subid>__<name>; legacy formats are accepted), broken-registration (missing/invalid boxmeta.toml in the local store), duplicate-box-id, stale-cache (boxyard_meta.json disagrees with a fresh scan), dangling-symlinks (group symlinks with missing targets), group-tree-debris (real files in the group tree, which break create-user-symlinks and thereby most mutating commands), orphaned-sync-records, interrupted-sync (sync records left incomplete — the local copy may be incomplete; re-sync to recover), unknown-storage-location (leftovers from removed/renamed storage locations), rclone-config (missing rclone binary/remote sections/default exclude file), stale-meta-mirror (remote boxmetas not mirrored locally — what sync-missing-meta would fetch; a machine where that never runs silently hides newer boxes from boxyard list), tombstoned-box (boxes deleted from another machine but still registered here), and tree-orphans (parents referencing unknown box ids).
Include, exclude, copy
Include an excluded remote box locally:
boxyard include --box-name NAME
boxyard include --interactive
Exclude a local copy while keeping the remote:
boxyard exclude --box-name NAME
boxyard exclude --interactive --show-sizes
boxyard exclude --box-name NAME --skip-sync
Copy a remote box to an arbitrary destination without adding it to Boxyard tracking:
boxyard copy --box-name NAME --dest ./NAME-copy
boxyard copy --box-name NAME --dest ./NAME-copy --meta --conf
boxyard copy --box-name NAME --dest ./NAME-copy --overwrite
Groups and hierarchy
Groups:
boxyard add-to-group --box-name NAME GROUP [OTHER_GROUP ...]
boxyard remove-from-group --box-name NAME GROUP [OTHER_GROUP ...]
boxyard list-groups --box INDEX_NAME
boxyard list-groups --all --include-virtual
boxyard create-user-symlinks
Parent-child hierarchy:
boxyard add-parent --box-name CHILD --parent-name PARENT
boxyard remove-parent --box-name CHILD --parent-name PARENT
boxyard tree --show-status
boxyard list --view tree --show-status
Rename, delete, and force operations
Rename:
boxyard rename --box-name OLD --new-name NEW --scope both
boxyard rename --box-name OLD --new-name NEW --scope local
boxyard rename --box-name OLD --new-name NEW --scope remote
Sync only the name between local and remote:
boxyard sync-name --box-name NAME --to-local
boxyard sync-name --box-name NAME --to-remote
Delete:
boxyard delete --box-name NAME
boxyard delete --box-name NAME --force # needed when the box has children
Destructive force push:
boxyard force-push --box-name NAME --source /path/to/source --force
Per-box sync configuration
Each box can have a conf/ folder. Boxyard syncs conf/ before data/, so filters travel with the box.
Special files:
conf/.rclone_include # only sync matching files
conf/.rclone_exclude # exclude matching files
conf/.rclone_filters # combined rclone filter rules
If conf/.rclone_exclude is absent, Boxyard uses:
~/.config/boxyard/default.rclone_exclude
Default excludes include .venv/, .pixi/, .trunk/, node_modules/, __pycache__/, and .DS_Store.
Shell helper
The repo includes a zsh helper:
source /path/to/boxyard/shell/boxyard.zsh
Default keybinding: Ctrl+G (BOXYARD_WIDGET_KEY can override it). Type a partial box name, press the keybinding, and it replaces the current word with a relative path to the selected box. It uses boxyard-shell-helper search and fzf for multiple matches.
Direct helper examples:
boxyard-shell-helper search TERM
boxyard-shell-helper search TERM --group GROUP
boxyard-shell-helper search TERM --included
boxyard-shell-helper search TERM --excluded
Reference files in this repository
From this skill directory, the repository root is ../...
Read these for more context when needed:
../../README.md— high-level usage and directory layout../../src/boxyard/const.py— default paths and constants../../src/boxyard/config.py— config model and derived paths../../src/boxyard/_cli/main.py— command definitions../../src/boxyard/_cli/multi_sync.py—multi-sync../../src/boxyard/_models.py— box path and metadata layout../../src/boxyard/_shell_helper.py— shell helper behavior