Copyright (C) 2026 pacnpal — released under LGPL-3.0-or-later.
Native macOS FAT32 partition + filesystem resizer. Grows the slot in the MBR partition table and the FAT32 inside it, in one shot, without needing libparted, GParted Live, or a Linux VM. Bare-FAT32 grows are also supported on individual slices and raw image files.
This tool directly rewrites partition tables and filesystem metadata. A wrong device path, an unplugged USB stick mid-write, or undetected hardware faults can destroy every byte on the target disk.
- ALWAYS take a
ddimage of the target disk first. The non-negotiable safety gate. fatgrow refuses to write without--i-have-a-backup. The flag is honor-system; you have to actually take the backup. - The tool refuses
/dev/disk0(your boot disk) outright. - Aimed squarely at external/removable media. Internal disks work too in theory; the boot disk is the only hard refusal.
- 1.0 covers FAT32 only. FAT16/12 grow, shrink, and GPT slot edits are in the roadmap but return clear errors today rather than guessing.
If any of the above scares you off, that's by design. Use at your own risk and don't skip the backup step.
| Capability | Status |
|---|---|
Inspect MBR + FAT BPB (--info) |
done |
| FAT32 grow on a slice or image | done |
| Combined MBR slot + FAT32 grow on disk | done |
| Resume from interrupted grow | done |
| Refuse dirty filesystems | done |
| Refuse target ≤ current | done |
| FAT16 grow | planned 1.1 |
| FAT12 grow | not planned |
| Shrink (any FAT type) | planned 1.2 |
| GPT slot edit | planned 1.3 |
| Cluster-size change | not planned |
| Item | Notes |
|---|---|
| macOS | 13.x (Ventura), 14.x (Sonoma), 15.x (Sequoia) tested |
| Architecture | universal binary: arm64 + x86_64 |
| Xcode Command Line Tools | only if building from source |
sudo |
required for any operation on a real /dev/diskN |
| Backup space | enough free space for a full dd of the target disk |
| External storage | the target disk; do not point at internal disks |
No Homebrew packages, no libparted, no dosfstools, no Python at runtime.
Tests use python3 (os.truncate) and newfs_msdos / hdiutil /
diskutil, all of which ship with macOS.
Download the universal binary from the releases page and unpack:
curl -LO https://github.com/pacnpal/fatgrow/releases/download/v1.0.0/fatgrow-1.0.0-macos-universal.tar.gz
curl -LO https://github.com/pacnpal/fatgrow/releases/download/v1.0.0/fatgrow-1.0.0-macos-universal.tar.gz.sha256
shasum -a 256 -c fatgrow-1.0.0-macos-universal.tar.gz.sha256
tar -xzf fatgrow-1.0.0-macos-universal.tar.gz
cd fatgrow-1.0.0-macos-universal
./fatgrow --versionQuarantine on download. Anything you download from a browser on macOS gets the
com.apple.quarantineextended attribute, which blocks Gatekeeper-unsigned binaries from running. fatgrow is not code-signed or notarized (no Apple Developer ID). Strip the quarantine flag yourself:xattr -d com.apple.quarantine ./fatgrowIf you'd rather keep the quarantine flag and click through the prompts, macOS will offer "Allow Anyway" once you try to run it under System Settings → Privacy & Security.
git clone https://github.com/pacnpal/fatgrow.git
cd fatgrow
make # native build for your CPU
# or:
make universal # arm64 + x86_64 fat binary
sudo cp fatgrow /usr/local/bin/ # optionalmacOS Sequoia tightened "Files and Folders" privacy controls. Even with
sudo, raw access to removable media can require an explicit grant.
On a fresh Sequoia install, the first run of fatgrow against an
external disk may show:
"Terminal would like to access files on a removable volume."
Approve. The grant persists per-app. If you skip the prompt, fatgrow's
read of /dev/rdiskN returns EPERM or EBUSY and the tool exits.
If the prompt never appears (typical for headless / SSH sessions), you may need to grant Full Disk Access to your terminal app:
System Settings → Privacy & Security → Full Disk Access
→ click + → add /Applications/Utilities/Terminal.app (or iTerm,
Warp, Ghostty, whatever shell you're driving)
→ toggle ON
→ restart the terminal app
This is a per-terminal-app, per-user grant. fatgrow itself doesn't request permissions — it inherits them from the calling shell.
./fatgrow --info path/to/image.img
sudo ./fatgrow --info /dev/disk4s1
sudo ./fatgrow --info /dev/disk4 # whole disk: prints MBR + probes FAT slots/dev/diskN is silently rewritten internally to /dev/rdiskN for raw
character-device access. /dev/disk0 (the boot disk) is refused outright.
# 1. ALWAYS take a backup first.
sudo dd if=/dev/rdisk4 of=$HOME/disk4-backup.img bs=4m status=progress
shasum -a 256 $HOME/disk4-backup.img > $HOME/disk4-backup.sha256
# 2. Print the grow plan, write nothing.
sudo ./fatgrow --grow --size max --dry-run /dev/disk4
# 3. Execute. The --i-have-a-backup gate is honor-system but mandatory.
sudo ./fatgrow --grow --size max --i-have-a-backup /dev/disk4
# 4. fatgrow itself runs unmount + fsck before exit, so no extra steps.--size accepts max, raw bytes, or any K/Ki/M/Mi/G/Gi suffix:
sudo ./fatgrow --grow --size 5G --i-have-a-backup /dev/disk4
sudo ./fatgrow --grow --size 4500M --i-have-a-backup /dev/disk4s1
sudo ./fatgrow --grow --size max --i-have-a-backup whole-disk-image.imgfatgrow inspects sector 0 of the target. If it parses as a FAT BPB, the
target is treated as a bare FAT volume (slice or unpartitioned image). If
not, fatgrow reads the MBR and operates in whole-disk mode. You can
override slot selection with --part N (1..4).
If the grow is interrupted (^C, unplug, kernel panic, power loss),
fatgrow leaves a checkpoint at /var/tmp/fatgrow-<volid>.ckpt. To
continue:
sudo ./fatgrow --grow --size max --i-have-a-backup --resume /dev/disk4The checkpoint is updated every 64 MiB during the data-move phase. If you
lose power mid-checkpoint and want to start over from your dd backup,
restore the backup first, then re-run without --resume.
--allow-dirty— proceed even if the FS isn't cleanly unmounted (FAT[1] ClnShutBit clear). Implies risk; only do this if you've already fsck'd.--no-progress— suppress the move-loop progress bar.-v/-d— verbose / debug logging.--part N— force operating on MBR slot N (1–4). For a whole disk with a single FAT32 slot, omit and fatgrow auto-picks.
sudo /usr/local/bin/fatgrow --grow --size max --i-have-a-backup /dev/disk4
MBR signature : 0xaa55 (valid)
Disk signature: 0x428c8b2d
Total sectors : 15924384
# Status Type Start LBA Sectors ...
1 * 0x0c 32256 6322176 ... FAT32 (LBA)
2 0x12 63 32193 ... Diagnostic
Target slot: 1 (start LBA 32256, current size 6322176 sec, type 0x0c)
New slot size: 15892128 sectors (7.58 GiB); +9569952 sectors absorbed
==== grow plan ====
total_sectors 6297984 -> 15892128
fat_sz (sec) 6139 -> 15505
clusters 785709 -> 1982635 (+1196926)
Data move required: 18732 sectors forward (3.00 GiB)
MBR slot 1 resized to 15892128 sectors and synced.
Checkpoint at /var/tmp/fatgrow-233c14d8.ckpt
[move] 100% (3.00 GiB / 3.00 GiB)
Move completed in 368 seconds.
Done. Volume grown to 15892128 sectors (7.58 GiB); +1196926 clusters added.
Settling filesystem state (unmount + fsck)...
/sbin/fsck_msdos -y /dev/rdisk4s1
** /dev/rdisk4s1
** Phase 1 - Preparing FAT
** Phase 2 - Checking Directories
** Phase 3 - Checking for Orphan Clusters
4934 files, 7499864 KiB free (1874966 clusters)
Whole-disk grow complete.
To mount: sudo diskutil mountDisk /dev/disk4
Briefly, since the algorithm is the part most likely to be wrong:
- Read the MBR. Find the FAT32 slot to grow.
- Compute the new geometry. Microsoft's fatgen103 §3.5 FAT-size
formula gives
new_fat_szfor the target total sectors. - Refuse if the FS is dirty (FAT[1] ClnShutBit, mask
0x08000000), if any slice is mounted, or if the target ≤ current. - Slot grow. Write the new MBR with updated
sector_countand ending CHS triple. (CHS uses the modern 63 sec/track × 255 head geometry, or the0xFE 0xFF 0xFFsentinel for LBAs out of CHS range.) - Read the entire old FAT1 into RAM (capped at 256 MiB).
- Move every data sector from
[old_first_data .. old_data_end]to[new_first_data .. new_data_end]. Processing high-LBA first prevents overlapping moves from clobbering unread data, sincedelta > 0means each destination is past every still-unread source. - Persist a checkpoint to
/var/tmp/fatgrow-<volid>.ckptevery 64 MiB so an interrupted grow can resume. - Write the new (extended) FAT1 and FAT2 — old contents at the start followed by zero-extension for the new free clusters.
- Patch the boot sector's
tot_sec_32andfat_sz_32, then mirror to the backup boot region (typically sec 6, 7, 8) per the FAT spec. - Update FSInfo's
free_countandnxt_free. - Sync the device, delete the checkpoint.
- Cleanup: force-unmount any auto-mounted slices and run
fsck_msdos -yso the user sees a guaranteed-clean post-state.
Cluster numbers do not change during a grow — only the cluster→LBA mapping shifts forward. So directory entries (which reference cluster numbers) and the FAT chain (which references cluster numbers) remain correct without any rewriting. This is the simplification that lets fatgrow do the job in ~2,000 lines instead of libparted's multi-thousand-line cluster-remapping algorithm.
The kernel's diskarbitrationd is mid-arbitration after a previous I/O
operation. fatgrow falls back to opening without exclusive lock and
warns. If you want strict locking:
sudo diskutil unmountDisk force /dev/disk4
sleep 2
sudo ./fatgrow --grow ...Some other process (often macOS auto-mount) holds the slice open. fatgrow
1.0 runs diskutil unmountDisk force + fsck_msdos -y automatically at
the end of every successful grow, so this should not happen on the
post-grow path. If you see it, manually:
sudo diskutil unmountDisk force /dev/disk4
sudo /sbin/fsck_msdos -y /dev/rdisk4s1
sudo diskutil mountDisk /dev/disk4Mounted FAT volumes have boot_flags & 0x01 set on disk (the
"currently-mounted" indicator). fsck_msdos of a mounted volume will
always report dirty. Force-unmount and re-run fsck.
After fatgrow exits, macOS re-arbitrates the disk and should pick up the
new MBR. If diskutil list still shows the old size:
sudo diskutil unmountDisk /dev/disk4
sudo diskutil eject /dev/disk4
# physically unplug + replug the USB stickYou're either missing sudo or you're on macOS 15+ without the Files &
Folders / Full Disk Access grant. See the "macOS 15 permission grants"
section above.
make smoke # --info on FAT16 + FAT32 images
make test-fs-grow # bare FAT32 grow on a 64 → 256 MiB image
make test-wholedisk-grow # combined MBR + FAT32 grow with file checksums
make test-safety # 4 refusal scenarios
make test # all of the abovetest-wholedisk-grow builds a 256 MiB image with a 64 MiB FAT32 slot plus
192 MiB of trailing free space (mimicking the typical "restored small
partclone image onto larger USB" scenario), writes ~20 MiB of test data
including random binary files and subdirectories, runs the combined grow,
then verifies:
- All file SHA-256 checksums survive the grow.
- macOS auto-mounts the post-grow slice cleanly.
fsck_msdos -npasses phases 1, 2, 3 with no errors.- Both FAT copies remain byte-identical.
- The backup boot sector mirror at sector 6 matches the primary.
CI runs the full test suite with -Werror enforced on macOS-13
(x86_64) and macOS-14 (arm64) GitHub Actions runners. See
.github/workflows/ci.yml.
GitHub Issues. Please include:
- macOS version (
sw_vers). - The output of
sudo fatgrow --info <target>(BEFORE attempting any destructive operation). - The exact command you ran and the full stderr.
- Whether you have a backup. If you don't, stop and take one before any further investigation.
GNU Lesser General Public License v3.0 or later.
The LGPL-3.0 incorporates the GPL-3.0 by reference; both texts are shipped in this repository (LICENSE and COPYING).
- Microsoft FAT32 File System Specification ("fatgen103") — the canonical reference used to verify FAT-size and BPB layout details.
- dosfstools (
fsck.fat,mkfs.fat) — provided ground truth for the FAT[1] flag-bit mask values. - GNU parted / libparted — fatgrow does not use libparted, but
libparted's
libparted/fs/r/fat/resize.cwas studied to understand the more general cluster-remapping algorithm. fatgrow uses the simpler "memmove + extend FAT" approach because it keepssec_per_clusconstant.
fatgrow is a clean-room reimplementation written in 2026. No upstream parted code was copied.