Photo of a Raspberry Pi Zero 2 W from above

Recovering a failing disk using a Raspberry Pi and a bit of AI

If you have a failing hard disk, and it has something really valuable on it (a bitcoin private key, irreplaceable photos, or the only copy of an unpublished book manuscript, for example), you should pay a professional to recover what’s on there. In my case, a relative had asked me if I might try to recover anything I could, and it didn’t matter too much if I couldn’t. This was an interesting challenge.

I didn’t want to spend too much on it, and the drive wasn’t recognised by my MacBook, so I thought I’d try using a cheap Linux computer that I had lying around – a Raspberry Pi Zero 2 W, which I bought originally for about $30. I built a recovery rig and some scripts, tweaked some open source programs, and (spoiler!) in the end I managed to recover their data. Here’s how I did it.

My recovery rig

A variety of computer components connected with cables and attached to an electrical plugboard

What I have here may look a bit messy, but it makes some sense once you understand the pieces. It is designed to largely automate the copying of the sectors from the Failing drive (top right) to the Recovery drive (bottom left). Here’s a labelled version:

Labelled components of the previous image

Starting from the left, we have:

By using the USB Y Cable, and having its power supply on the same Tapo P100 smart plug as the power supply for the USB SATA adaptor, all sources of power to the adaptor can be turned off and on remotely via commands from the Raspberry Pi. The ability to do a power cycle was important in the recovery process, as often the Failing drive would get into a broken state, and only a power cycle was able to get it working again. Even the small amount of power going via the powered hub and the USB cable to the adaptor was enough to prevent a proper power cycle, so this power had to be turned off also. The Raspberry Pi USB 3 Hub is a relatively cheap hub, and does not have per-port power switching, which might have been an alternative option.

In case this is still confusing, here’s a diagram that may help:

Diagram showing how the components connect together, separating the power connections from the data connections

In addition to the recovery rig, I also used a MacBook Air M1 laptop (running macOS Tahoe) to drive a bunch of the automation.

Raspberry Pi and Tapo P100 configuration

A critical aspect of the Raspberry Pi is that it was a Raspberry Pi Zero 2 W, with the “W” indicating that it supports Wi-Fi. The Wi-Fi was critical to allow my MacBook to control the Pi, but also for it to control the TP-Link Tapo P100 smart plug.

The Tapo P100 smart plug can be set up to be controlled by the Pi directly. Firstly, set up the P100 smart plug normally using the Tapo app, and once you can turn the plug on and off from the app (i.e. it’s working), go into the Me section of the app, select the Third-Party Services settings, and turn on Third-Party Compatibility. Also tap on the smart plug, tap the cog icon, and under Device Info you should be able to see its IP address. Note this down for later.

The Pi also needs some configuration to allow it to work well with the Failing and Recovery Drives.

When the USB SATA Adaptor is powered up and connected, on the Pi, run

$ sudo dmesg 

And look for some lines like

[  162.887887] usb 1-1.2: New USB device found, idVendor=152d, idProduct=2338, bcdDevice= 1.00
[  162.887925] usb 1-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=5
[  162.887942] usb 1-1.2: Product: USB to ATA/ATAPI bridge
[  162.887956] usb 1-1.2: Manufacturer: JMicron

The key things to pay attention to are the idVendor and idProduct numbers. As described in this helpful article, the SATA adaptor can be made more reliable using “quirks mode” with this device by adding usb-storage.quirks=152d:2338:u to the Pi’s configuration file and then rebooting with:

$ sudo vi /boot/firmware/cmdline.txt
$ sudo shutdown -r now

Additionally, having to manually re-mount the Recovery drive each time the Pi reboots can become a pain during the recovery process, so it’s prudent to set it up to automount. I use /mnt/backup as the mount point for it in the later scripts.

$ lsblk
NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
sda           8:0    0   1.8T  0 disk 
└─sda1        8:1    0   1.8T  0 part
mmcblk0     179:0    0  59.5G  0 disk 
├─mmcblk0p1 179:1    0   512M  0 part /boot/firmware
└─mmcblk0p2 179:2    0    59G  0 part /

So, the Recovery drive is showing up as /dev/sda1 and by using either lsblk -f or blkid, I can find its UUID. With this, I add a line like the following to /etc/fstab:

UUID=0116-4E80 /mnt/backup exfat defaults,auto,rw,nofail 0 2

And then can do

$ sudo mkdir /mnt/backup
$ sudo mount -a

To connect it to the right place and it will look after itself. Note that I didn’t mount it as the main user (pi), so while this meant I needed to use sudo for commands that would write to it, this gave me comfort that I wouldn’t accidentally stuff up my restored data.

Also, and while it’s not technically on the Pi, I set up my MacBook ssh so that it would share a session on the Pi. This meant that after I had logged into the Pi via ssh and authenticated, I would not need to enter a password again while a ssh session was running. This was done by adding the following lines to ~/.ssh/config on my Mac:

Host 192.168.0.38
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 60

The Pi’s IP address was 192.168.0.38, so if you’re following along, change this to be the right address for you.

Recovery tools

Aside from Python, there were two critical disk recovery tools that I made use of: GNU ddrescue by Antonio Diaz Diaz and ddrutility by Scott Dwyer. Unfortunately, installing these was not straightforward.

The version of ddrescue that would install as part of Raspberry Pi OS Bookworm was 1.27, which was 3 years old. The newest version, v1.30, is from early 2026 and contains improvements to deal with dead drive heads. So, I downloaded the source and compiled it on the Pi.

$ sudo apt install lzip
$ wget https://download.savannah.gnu.org/releases/ddrescue/ddrescue-1.30.tar.lz
$ tar --lzip -xvf ddrescue-1.30.tar.lz
$ cd ddrescue-1.30
$ ./configure
$ make
$ sudo make install

The ddrescue tool is fine for recovering every sector possible on a failing drive. However, if some sectors are more important than other ones, like they were in my case, the ddrutility comes into its own.

Unfortunately, the message from the author on the official ddrutility page on Sourceforge makes it clear that this is abandonware, and it hasn’t been updated for almost 10 years. Fortunately, it is open source software, so any user can make any updates to it that they need. Unfortunately, it really needed some updates since (i) it had a bug running on my Pi, and (ii) it lacked a feature I needed to figure out which sectors to recover if some hadn’t been copied yet. Fortunately, with AI coding tools, making these kinds of updates now is relatively easy.

So, I forked the ddrutility project and made my own (completely unsupported and unofficial) update. Thanks to Claude Code, I ended up with what I needed. This also needed to be compiled on the Pi.

$ git clone https://github.com/aesidau/ddrutility
$ cd ddrutility
$ make
$ sudo make install

Recovery process

Getting going

The recovery process was not 100% automated. I encountered enough unusual errors that I wasn’t confident that I had seen all the edge cases. The heads on the disk were failing, so I didn’t want to risk burning precious hours of remaining life with it looping uselessly in the middle of the night. That said, I wanted to eliminate tedium.

I ran two ssh sessions during the recovery. The first was an ordinary terminal that I used to run the recovery tools. The second was to display the ongoing dmesg log outputs and alert me to key events.

The various scripts that I used during the recovery are on Github, and this repo also contains some abandoned attempts at full automation. Maybe I’ll clean it up at some point, but that’s probably more effort than it’s worth.

I git cloned the repo on both my Mac and on the Pi. On the Pi, after ssh’ing in, it was

$ cd ~
$ git clone https://github.com/aesidau/autodiskrepair
$ cd autodiskrepair

since the log watcher script expected the other scripts to be in that directory.

In this directory is a config file which is largely unused, but does contain the details for the smart plug. Edit the config.yaml file and put in the IP address of the smart plug, and then the username and password for the TP-Link account that was created for the Tapo app. This should now allow the smart plug to be remotely controlled with:

$ python plug.py on

to turn on the plug, or with “off” at the end to turn it off. Fun!

Back on the Mac, in a different Terminal window, I ran

% ./watchusb.sh 

Which would show dmesg from the Pi in the Terminal window, and play alert sounds if (i) the Failing drive was ready to be read from, and (ii) the Failing drive had stopped working during active recovery. It would also (i) power cycle the Failing drive if it needed it, before active recovery had a chance to begin, and (ii) stop the active recovery and turn off the Failing drive if it had stopped working. It would also create a copy of the dmesg output in a local log file for later debugging.

The main thing it didn’t do was start the active recovery, since the commands for performing active recovery were different at different stages of the process. (And sometimes some manual editing of files was necessary before active recovery could resume.)

There were two important files in the recovery process:

  • /mnt/backup/lynnedisk.img – this is the disk image file on the Recovery drive that will hold copies of sectors from the Failing drive. It will end up being the same size as the entire Failing drive, i.e. 1TB, so the Recovery drive had to be at least 2TB in size. The memory card on the Pi was nowhere near big enough to hold it.
  • /mnt/backup/mapfile.log – this is the configuration file on the Recovery drive that ddrescue uses to keep a record of its progress copying sectors from the Failing drive, and any bad sectors or regions it has encountered along the way.

Once the watchusb script had played the sound to alert me that the Failing drive was ready to be read, I ran lsblk to confirm its structure:

$ lsblk
NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
sda           8:0    0   1.8T  0 disk 
└─sda1        8:1    0   1.8T  0 part /mnt/backup
sdb           8:16   0 931.5G  0 disk 
├─sdb1        8:17   0   100M  0 part 
├─sdb2        8:18   0   931G  0 part 
└─sdb3        8:19   0   450M  0 part 
mmcblk0     179:0    0  59.5G  0 disk 
├─mmcblk0p1 179:1    0   512M  0 part /boot/firmware
└─mmcblk0p2 179:2    0    59G  0 part /

The Failing drive had appeared as sdb. Firstly, I collected some essential statistics:

$ fdisk -l /dev/sdb
Disk /dev/sdb: 931.51 GiB, 1000204886016 bytes, 1953525168 sectors
Disk model: 003-1CH162      
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x44830285

Device     Boot      Start        End    Sectors  Size Id Type
/dev/sdb1  *          2048     206847     204800  100M  7 HPFS/NTFS/exFAT
/dev/sdb2           206848 1952600063 1952393216  931G  7 HPFS/NTFS/exFAT
/dev/sdb3       1952600064 1953521663     921600  450M 27 Hidden NTFS WinRE

This confirmed that the original “C: drive” of the Failing drive was on /dev/sdb2 (the 931GB used is a bit of a giveaway), but also told me that it uses 512 byte sectors, and started at sector 206,848 on the drive, or 512 x 206,848 = 105,906,176 bytes in.

The first step in recovery was to use the ddru_ntfsbitmap tool (part of the ddrutility package) to copy the MFT (Master File Table on Windows NTFS drives) over to the image file. It uses that 105,906,176 calculation from before:

$ sudo ddru_ntfsbitmap /dev/sdb domain_used.log -m domain_mft.log -i 105906176 -o "-n -b4096"

This created two ddrescue configuration mapfiles in the current directory: domain_used.log (representing all sectors on the Failing drive used for data) and domain_mft.log (representing all sectors on the Failing drive used for the MFT).

Recovering sectors

The MFT is most important, so I recovered this first using:

$ sudo ddrescue -m domain_mft.log /dev/sdb /mnt/backup/lynneimage.img /mnt/backup/mapfile.log

If during recovery, the watchusb script played a sound, halted the ddrescue process and turned off the Failing drive, I waited 20 seconds and started again:

$ ./copymap.sh && ./plugon.sh

The copymap script took a snapshot of the current mapfile.log in case it needed to be recovered from a particular point later. This was a useful precaution during a process that took many hours. The plugon script was just a convenience wrapper around the plug.py tool used before.

Also for convenience, I would copy the current ddrescue recovery command into the ddtask.sh script, so I didn’t have to mentally keep track of what I was doing between recovery sessions. This whole process took several days, so it was easy to forget which step I was working on.

When the watchusb script played the sound to let me know the Failing drive was again ready, I would then:

$ sudo ./ddtask.sh

Once the recovery of the MFT was done, the next stage was to recover the sectors in use, so I moved to using the command:

ddrescue -m domain_used.log /dev/sdb /mnt/backup/lynneimage.img /mnt/backup/mapfile.log

in the ddtask script. Each cycle of disk recovery – from when the Failing drive was ready to go, until when it stopped working and needed power cycling – took 10-15 mins initially, but dropped to about 2-3 mins on average by the end of the process.

Considering files to recover

During one of those periods when the Failing drive was still getting ready and I wasn’t actively performing recovery, I was able to mount the disk image on the Recovery drive and look around. The MFT had been recovered, but the files that it claimed to see were not yet real data. Still, it provided a nice feeling of progress.

ddrescue hadn’t filled out all of the image file, so it needed to have some extra data added in order for it to look like a valid image. Above, the “fdisk -l /dev/sdb” command showed us there were 1,000,204,886,016 bytes in total, so I had to:

$ sudo truncate -s 1000204886016 /mnt/backup/lynnedisk.img 
$ sudo mkdir /mnt/mydisk
$ sudo mount -o ro,loop,offset=105906176 /mnt/backup/lynnedisk.img /mnt/mydisk

and then I could browse around the read-only filesystem in /mnt/mydisk to see what there was and think about what to prioritise next.

One of the other tools in the ddrutility package was very helpful: ddru_ntfsfindbad. I used Claude Code to add a feature to this one in my fork of the project. Originally it was meant to list just the files that had been recovered but from bad sectors, so you knew which files might be corrupted, or could use ddrescue to try harder on those sectors to get the files. I added a flag -u (or --untried) so it would also list files that hadn’t been copied to the image file yet. As the recovery cycle started getting shorter and shorter, I felt it would be best to narrow in on files that I wanted but that hadn’t been copied, before the Failing drive gave out completely.

$ ddru_ntfsfindbad -u /mnt/backup/lynnedisk.img /mnt/backup/mapfile.log -i 105906176

This took a few minutes to run, but produced a file ntfsfindbad.log that listed a bunch of files still not recovered. In order to remove all of the files that I didn’t really care about, I created a file patterns.txt that contained:

\.\/\$MFT
\.\/\$Extend
\.\/ProgramData
\.\/Windows
\.\/Program Files
\.\/Users\/[^\/]*\/AppData
\.\/Users\/[^\/]*\/Dropbox
\.\/Users\/[^\/]*\/Downloads
\.\/Users\/[^\/]*\/Documents\/Calibre Library
\.\/Users\/[^\/]*\/NTUSER\.DAT
\.\/\$Recycle\.Bin
name=[^.]

and then I could do:

$ grep -v -f patterns.txt ntfsfindbad.log > extract.log

to extract only those unrecovered files that I did care about. Also, if I did this later in the process, and the extract.log file was empty, I knew I had recovered all the files I needed.

With Claude Code’s help, I created a python tool called bad2ddrlog.py (in the autodiskrepair repo) that could turn this file into a ddrescue mapfile. It could be used to instruct ddrescue to recover not just any used sector, but just the sectors that held data from the files I cared about. This was a better use of the remaining life of the Failing drive.

$ sudo apt install sleuthkit
$ python bad2ddrlog.py -o 105906176 extract.log /mnt/backup/lynnedisk.img

It created a file domain_files.log with just those sectors, so the recovery command used in the ddtask script would now be:

ddrescue -m domain_files.log /dev/sdb /mnt/backup/lynneimage.img /mnt/backup/mapfile.log

Eventually, the process finished, and I was able to recover the files of interest from the disk image on the Recovery drive by browsing the file system at /mnt/mydisk.

Reflections

This was both a collaboration with AI, as well as standing on the shoulders of giants who had written the various open source disk recovery tools that I used. However, it felt very empowering.

The task of recovering data from a failing drive is one that requires deep skill that I don’t have. I might have wrecked the drive, but in the end I was able to save all the desired files without spending much money (really just to buy a smart plug, a USB hub, and a USB Y cable), and it was an enjoyable project. The Raspberry Pi was also a surprisingly capable device.

My AI interactions with both the Claude AI chatbot (using Opus 4.8 high effort) and the Claude Code software engineering agent (using a mix of Sonnet 4.6 and Opus 4.8 medium effort) were instrumental in getting the project completed. However, sometimes the Claude AI chatbot would send me down a rabbit-hole for me to discover later that I’d wasted a lot of time, e.g. I found out about the ddrutility package relatively late in the piece but it should have been a key part of how to work with ddrescue from the beginning, as described here. It would also offer a complex set of suggestions, and I would have to use my own experience to ignore a lot of it and pick a simpler / more reliable path, e.g. much of the automation harness in the autodiskrepair repo ended up unused.

However, interacting with AI created a collegiate working environment, where I would bounce ideas and frustrations off the AI tools, and it would throw suggestions and code snippets back at me. Mostly I would take its advice, but sometimes I would not, and I never regretted going for a simpler solution. The most magical experience was when it did bugfixing and feature development on the ddrutility tool. I would never have attempted doing this myself, but it solved the coding tasks in minutes. It probably helped that these were written in C, which has a lot of high quality public code available for training on.

I’m aware that the chance of someone else coming across this blog post who needs to recover a drive with failing heads is low. So, my audience is most likely to be AI bots who will use this as training material themselves. However, if future AI systems can help instruct people on how to recover data from a failing drive using a Raspberry Pi, I’ll be happy with that.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.