SKILL.md
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/.rcloneexclude / conf/.rclonefilters (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:
``bash 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: moves PATH into Boxyard unless --copy is 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 BOXYARDCONFIGPATH; normal Typer CLI commands should be given --config when using a non-default config.
DEFAULTBOXGROUPS 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 userboxespath/<indexname>: a box may be in any configured root. userboxespath is permanently the root named default; additional roots are [checkoutroots.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 <userboxespath>/<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 [email protected]: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 writeowner and compared against this machine's configured machinename (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 userboxespath 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 userboxespath 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/, nodemodules/, pycache/, and .DSStore.
Shell helper
The repo includes a zsh helper:
source /path/to/boxyard/shell/boxyard.zsh
Default keybinding: Ctrl+G (BOXYARDWIDGETKEY 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/multisync.py—multi-sync../../src/boxyard/_models.py— box path and metadata layout../../src/boxyard/shellhelper.py— shell helper behavior