braid add
Add one or more disks to the braid pool. Creates a new pool if none exists, or expands an existing one.
When to use it
- Setting up a new NAS (bootstrap with one or more disks)
- Expanding storage by adding a new drive to an existing pool
Basic example
sudo braid add toshiba1=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_1234
Common variations
Bootstrap a new pool with two disks (creates RAID1 immediately):
sudo braid add \
toshiba1=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_1234 \
toshiba2=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_5678
Add a disk to an existing pool:
sudo braid add toshiba3=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_9012
Preview what would happen without making changes:
sudo braid add toshiba1=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_1234 --dry-run
Skip the confirmation prompt (for scripting):
sudo braid add toshiba1=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_1234 --yes
Pass passphrase non-interactively:
echo -n 'hunter2' | sudo braid add toshiba1=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_1234 --passphrase-stdin
sudo braid add toshiba1=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_1234 --passphrase-file /tmp/pass.txt
Enroll a keyfile for auto-unlock from a mounted USB drive at the same time:
sudo braid add toshiba1=/dev/disk/by-id/ata-TOSHIBA_MN07ACA12T_1234 --enroll /mnt/usb
Mount the USB first so the --enroll directory refers to removable media,
not persistent host storage.
Important flags
| Flag | Purpose |
|---|---|
--dry-run | Show what would happen without executing |
--yes | Skip interactive confirmation |
--passphrase-stdin | Read passphrase from stdin instead of TTY prompt |
--passphrase-file <path> | Read passphrase from a file (conflicts with --passphrase-stdin) |
--enroll <dir> | Enroll braid.key from this directory into LUKS slot 1 on each adopted disk – fresh or returning – whose slot 1 is empty; idempotent skip if the keyfile already authenticates slot 1 |
--luks-format-arg=<ARG> | Advanced: pass one raw argument to cryptsetup luksFormat, repeated once per argument; always use the equals form (e.g. --luks-format-arg=--pbkdf --luks-format-arg=argon2id). braid refuses flags it manages itself – identity, key-material, integrity, hardware-encryption (OPAL), and on-disk-layout options such as --uuid, --label, --type, --key-file, and offset/sizing flags. It also refuses a bare --, which would make cryptsetup ignore every argument after it, and options that make cryptsetup exit without formatting (--help, --usage, --version, --test-args, -?, -V). braid passes the disk to cryptsetup ahead of every --luks-format-arg, so no argument can change which disk is formatted; cryptsetup ignores a stray value that no option takes. Before it writes pending-op.json, braid runs the full luksFormat command with --test-args, which checks the options and writes nothing, and refuses with cryptsetup’s message if cryptsetup rejects them. |
--progress auto|always|never | Control progress display (default: auto) |
Disk spec format
Each disk is specified as NAME=PATH, where:
- NAME is a short label you choose (e.g.
toshiba1) - PATH is a
/dev/disk/by-id/link to a whole disk, not a partition. A disk has several links (ata-,wwn-, …); pass one of them, once.
The name is stored in pool.json and used in LUKS mapper names (braid-toshiba1), LUKS labels, and all future commands. The persistent member identity is the LUKS UUID, not the name.
What happens under the hood
-
Probes each disk to determine its state (fresh, braid-labeled, or foreign)
-
Shows a confirmation prompt with the disk’s name and by-id path, plus its model/size/serial (best-effort from the live device via lsblk – omitted if unavailable). This interactive prompt is skipped by
--yesand not shown in--dry-run. -
For fresh disks: pre-generates a LUKS UUID, LUKS-formats the disk with the pool passphrase and
braid-<name>label, enrolls the--enrollkeyfile into slot 1 if provided, creates a LUKS header backup, and opens the LUKS mapperSee Pending LUKS header backups – copy each
.luksheaderoff-system and delete the local copy. -
If no pool exists: creates a btrfs filesystem (RAID1 if 2+ disks, single if 1 disk; braid explicitly pins the
block-group-treefeature bit so that bit is visible and stable across toolchain versions – see ADR-027) -
If a pool exists: writes a phased UUID-keyed journal, adds the device to the existing btrfs filesystem, records the new membership in pool.json, then advances the journal to the balance phase
-
If the pool now has 2+ disks: balances data to RAID1, then clears the journal – unless the pool has a missing device, in which case the balance is skipped (a
[skip]note explains why). Redundancy is restored later byremove-missingorreplace, not by the degraded add.
Keyfile enrollment (--enroll DIR): braid enrolls braid.key into LUKS slot 1 on every adopted disk – fresh or returning. On a fresh disk slot 1 is always empty, so the keyfile is always added. On a returning braid disk braid first probes the keyfile: if it already authenticates slot 1 the enrollment is an idempotent skip with no slot change and no new header backup; if slot 1 is empty the keyfile is added. (If slot 1 holds a different, unknown key braid refuses – see Safety checks.) The keyfile is added before the header backup so the backup captures slot 1.
A sleep inhibitor is held during all irreversible operations to prevent the system from suspending mid-operation.
If a btrfs exclusive operation (a running balance, device add/remove/replace, resize, or swap activate) is already in flight on the pool, braid does not fail – its btrfs commands queue behind the in-flight operation (via --enqueue) and the kernel runs them when the pool is free. A paused balance is the exception and is refused (see Safety checks below).
Disk acceptance rules
braid classifies each disk before acting:
- Fresh disk (no LUKS): accepted. LUKS-formatted with the pool passphrase.
- Returning braid disk (not in
pool.json, braid-labeled LUKS, btrfs FSID matches the pool): accepted as a recovery add. The disk is re-joined to the pool without reformatting. If the old btrfs signature would makebtrfs device addrefuse the disk, braid first runs the narrow wipewipefs --all --types btrfson the verified mapper, then usesbtrfs device add -f. - Non-braid LUKS: refused. braid will not adopt a LUKS device it did not create.
- Braid-labeled, wrong pool: refused. The disk belongs to a different btrfs filesystem.
- Braid-labeled, no btrfs superblock: refused. The disk’s identity is ambiguous (could be partial init, clean eviction, or manual wipe). Wipe the disk and add as fresh.
- Braid-labeled, pool not mounted: refused during bootstrap. Identity cannot be verified without a mounted pool.
Safety checks / refusal cases
-
Rejects duplicate disk names in the same command
-
Rejects two disks that are the same disk: one by-id link passed twice, or two links (such as
ata-...andwwn-...) that point to one device. Pass each disk once. -
Rejects a disk that is not a whole disk. braid asks the kernel (sysfs), not the link’s name, so it refuses a partition even when its link has no
-partNsuffix, and refuses a device sysfs cannot answer for. Pass the disk’s own by-id link. These checks run when planning, so--dry-runrefuses too. -
Rejects a name that a pool member already uses with a different by-id path.
-
Rejects a by-id path that a pool member already uses under a different name.
-
Rejects a disk whose LUKS UUID matches another disk in the same command, a
pool.jsonmember listed under another name and by-id, or a device already live in the pool. This usually means add-cloned disk is attached; detach the cloned or unintended disk and retry. -
A disk that
pool.jsonalready lists under the same name and by-id path is that member, not a name or by-id conflict. If the member is live in the mounted pool on the same disk, add skips it, and confirms and adds only the other disks. When add skips every disk you named, it printsNothing to do -- <names> already in pool.and writes no journal. Any other such member refuses the whole command, with the next step for its state:- Its LUKS mapper is open but its disk is gone (hot-unplugged): the reopen route
braid removealso gives. Runbraid lockthenbraid unlockif the disk is back, orbraid lockthenbraid unlock --allow-degradedif it is gone. - No open mapper in the pool holds it, but its disk is attached with the member’s LUKS UUID, such as a disk that came back after a degraded unlock. btrfs does not take a returning disk back into a mounted pool, so run
braid lockthenbraid unlock, thenbraid doctor. - Its disk is not present. If the disk is dead, replace the member with
braid replace. Otherwise plug it in, then runbraid lock,braid unlockandbraid doctor. - Its disk is attached but is not LUKS, or carries another LUKS UUID.
braid unlockcannot open the member from it, so replace the member withbraid replace. - Another disk in the pool carries its LUKS UUID: refused as a duplicate LUKS UUID, as above.
- Its by-id link is gone while the member is live: refused as not present.
The reopen steps add
--allow-degradedwhile another member is missing. A skipped member gets no label or--enrollcheck;braid enrollenrolls a keyfile on existing members. These checks run when planning, so--dry-runrefuses too. - Its LUKS mapper is open but its disk is gone (hot-unplugged): the reopen route
-
Refuses when something other than btrfs is mounted at the pool mount point (
braid.mountPoint):<mount point> is already mounted with <fstype>, not btrfs. Unmount it first.Unmount it, then re-run. This check runs when planning, so--dry-runrefuses too. -
Refuses when
pool.jsonlists members but the pool is not unlocked (nothing is mounted at the pool mount point), so add never mistakes a locked pool for a new one. Runbraid unlockfirst, with--allow-degradedif a member is missing, then re-runbraid add; the missing-devices bullet below covers the repair. Ifpool.jsonis stale, back it up and move it aside, attach every intended member, then runbraid discover --writeto rebuild it. The rebuild leaves out any member that is not attached. This check runs when planning, so--dry-runrefuses too. -
Refuses when a pool is mounted at the pool mount point but
pool.jsonis missing or lists no members, so add never writes apool.jsonthat lists only the new disks. Attach every intended member, runbraid discover --writeto rebuildpool.json, then re-runbraid add. If an emptypool.jsonexists, back it up and move it aside first, becausediscover --writerefuses any validpool.json. This check runs when planning, so--dry-runrefuses too. -
Refuses when the mounted pool holds an open LUKS mapper whose LUKS UUID
pool.jsondoes not list, whether its disk is present or gone. The error names each such mapper and its UUID. Either the mount is not the braid pool, orpool.jsonis stale;braid doctorhelps tell which. For a stalepool.json, back it up and move it aside, attach every intended member, then runbraid discover --write. This refusal also covers naming a live disk thatpool.jsonleaves out, which does not printNothing to do. A device btrfs reports as MISSING with no open mapper does not trigger it, so a degraded add still runs. This check runs when planning, so--dry-runrefuses too, and again just before the journal. -
Rejects absent disks (not plugged in)
-
Verifies the passphrase against an existing pool member before formatting new disks
-
Refuses when cryptsetup rejects the
--luks-format-argoptions for a fresh disk. braid checks them withcryptsetup luksFormat --test-args, which writes nothing, when planning, so--dry-runrefuses too. cryptsetup checks the options themselves, not whether the kernel supports a cipher or hash they name, so such an option can still fail the format after the journal;braid recoverthen drops that disk (see Interrupted adds). -
Refuses a fresh disk that is not ready to format, both when planning (so
--dry-runrefuses too) and again just before the journal: if its/dev/mapper/braid-<name>mapper is already open, close it withsudo cryptsetup close braid-<name>; if a mount, an LVM volume group, an md array, or a device-mapper device holds the disk, free it (lsblk <by-id path>shows the holder). Then re-run. -
Warns if the pool has missing devices but does not refuse:
braid addstill adds the new disk, but skips the RAID1 convert balance (it surfaces a[skip]note), so the pool stays degraded and redundancy is not restored at add time. To repair, either runbraid replace --old <missing-name> --new <new-name>=/dev/disk/by-id/<...>to swap in a new disk for the missing member, or – on a 2-disk degraded pool, whereremove-missingalone would leave one disk with no redundancy – runbraid addthenbraid remove-missingto drop the dead member and rebalance onto the new disk. -
Warns if existing pool drives have a keyfile but
--enrollwas not passed -
With
--enroll, refuses if an adopted disk’s LUKS slot 1 is occupied by an unknown key the keyfile does not authenticate – remove it first withcryptsetup luksKillSlot, then retry. -
Refuses if a pending operation journal (
pending-op.json) exists – runbraid recoverto reconcile. -
Refuses if another braid operation is in progress (pool lock
/run/braid-pool.lockis held) – retry once it finishes. -
Refuses if a btrfs balance is paused on the pool – resume or cancel it first. A paused balance holds the exclusive-operation lock indefinitely, so braid cannot wait it out.
-
Refuses if the pool is mounted read-only. If an operator or a tool remounted it,
mount -o remount,rw <mount-point>fixes that; if btrfs forced it read-only after an error, only a fresh mount does (braid lock, thenbraid unlock). See Pool is mounted read-only. -
Refuses when UPS support is enabled and
braid ups statuscannot verify a trustedOL(utility-power) state.
Interrupted adds
braid recover never formats a disk.
A bootstrap add creates the pool only when mkfs.btrfs runs, after it formats every disk. If it stopped before that, there is no pool to recover: braid recover clears the journal and lists each disk luksFormat wrote. Wipe those disks (sudo wipefs -a <by-id path>) before you re-run braid add, since add refuses a braid-labeled disk with no btrfs. With the pool not mounted, recover checks every journaled disk first: if one is gone, it refuses and keeps the journal until you reconnect it, even if mkfs.btrfs ran. What happens under the hood has the details.
Existing-pool adds recover in two phases:
-
PoolMutation: disk preparation and btrfs membership are not fully committed yet. For each journaled target that is not a live pool member yet,
braid recoverfinishes the stepsbraid addplanned for that kind of disk:- Fresh disk: if the disk is still not LUKS, luksFormat never wrote it, so btrfs never held it. Recover drops it from the journal; re-run
braid addfor it. If no target remains, recover writes the pre-operationpool.jsonand clears the journal. If the disk carries thebraid-<name>label and the journaled LUKS UUID, recover enrolls the keyfile if the journal records one, backs up the LUKS header, opens the mapper, and runsbtrfs device add. - Returned disk: recover opens the mapper if it is closed. If the mapper still shows a btrfs signature, recover checks that it carries this pool’s FSID. If the journal records a keyfile, recover enrolls it and backs up the LUKS header. Then it runs the narrow btrfs-signature wipe and
btrfs device add -f.
See Pending LUKS header backups – copy each
.luksheaderoff-system and delete the local copy. Thebraid recoversafety checks say when recover refuses a target and keeps the journal. - Fresh disk: if the disk is still not LUKS, luksFormat never wrote it, so btrfs never held it. Recover drops it from the journal; re-run
-
PostAddBalanceRaid1: membership and
pool.jsonare committed.braid recoverwill not format, wipe, or add disks in this phase; it only mounts/probes the committed pool, repairspool.jsonfrom the committed live topology if needed, runs the owed RAID1 balance when btrfs balance state is idle and the pool can host RAID1 (no member missing or hot-unplugged, two or more present), and clearspending-op.jsonafter that succeeds or skips. A paused, running, or unknown balance state fails closed with the journal preserved.
Related commands
- braid status – check pool health and disk info
- braid remove – remove a live disk from the pool
- braid replace – replace a dead or live disk
- braid unlock – open LUKS devices and mount the pool
Related guides
- Getting started – initial setup walkthrough