The Frida hooks could see Bluetooth traffic. Method calls, timestamps, characteristic UUIDs. But the actual bytes? Those lived inside iOS NSData objects, and claude spent hours failing to extract them.
Nine different methods. Nine TypeErrors. The data was visible the whole time.
TypeError: Not a Function
The first attempt used the .bytes property on NSData. Frida threw a TypeError. claude tried getBytes:length:. TypeError. Then enumerateByteRangesUsingBlock:. TypeError. Then Memory.readByteArray. “Error: expected a pointer.”
Nine different extraction methods. Nine failures. Here’s what the logs looked like:
[SYSTEM] CBPeripheral writeValue Characteristic UUID: F1897EB9-A9AA-40F0-8CE6-2FA7915BC459 Could not read data bytes: TypeError: not a function Data description: {length = 2, bytes = 0x0100} ALT1 extraction failed: Error: expected a pointer ALT3 extraction failed: TypeError: not a function ALT4 extraction failed: TypeError: not a function ALT5 description parsing failed: TypeError: not a function ❌ ALL DATA EXTRACTION METHODS FAILED - CRITICAL DATA LOST!That message appeared in the logs over and over. Twenty times per session, sometimes more. claude would update the Frida script with a new approach, I’d run it, same result.
The full extraction attempt history:
| Method | Approach | Result |
|---|---|---|
| 1 | .bytes property | TypeError: not a function |
| 2 | getBytes:length: | TypeError: not a function |
| 3 | enumerateByteRangesUsingBlock: | TypeError: not a function |
| 4 | Memory.readByteArray | Error: expected a pointer |
| 5 | Raw objc_msgSend | Silent failure |
| 6 | Base64 encoding | TypeError: not a function |
| 7 | NSKeyedArchiver | TypeError: not a function |
| 8 | Direct pointer arithmetic | Pointer error |
| 9 | Description string parsing | Worked |
Nine methods. Eight failures. The data was “critically lost” according to the logs.
Except it wasn’t.
The Description Was Right There
Here’s what the logs contained:
Value: {length = 23, bytes = 0x283f3b263044eef20a00000004000000e907091004080e}The ObjC description string. It printed the hex bytes right there in the output. claude’s extraction methods were failing, but the data was visible the whole time (just in a different format than expected).
This went on for hours. On and off, across multiple sessions. claude kept iterating on extraction techniques: Base64 encoding, NSKeyedArchiver, raw pointer arithmetic. All failures. All unnecessary.
Eventually I pointed it out: “You already have the data. It’s in the description string. Stop trying to extract it differently and just parse what you have.”
claude pivoted immediately:
var desc = data.description();var hexMatch = desc.match(/bytes = 0x([0-9a-fA-F]+)/);if (hexMatch && hexMatch[1]) { var hexStr = hexMatch[1]; var bytes = []; for (var i = 0; i < hexStr.length; i += 2) { bytes.push(parseInt(hexStr.substr(i, 2), 16)); } console.log(" ✅ RECOVERED DATA FROM DESCRIPTION STRING!");}Regex on the description, pull the hex, done. The “critical data loss” was never real.
Tunnel Vision
This was the clearest example of a pattern I saw throughout the project. claude is excellent at execution. Give it a well-defined task and it will iterate relentlessly. But that same persistence becomes a liability when the task itself is wrong.
claude was so focused on the “right” way to extract NSData bytes. The working way was sitting right in front of it. It needed someone external to say “step back, look at what you actually have.”
The lesson: AI agents can get tunnel vision. They benefit from a human who can see the bigger picture and redirect when needed.
When Frida Wasn’t Enough
At one point, we needed to trace how the app calculated a “mystery byte” in the crypto challenge. Frida couldn’t see deep enough into the runtime. claude suggested dropping into LLDB.
The collaboration pattern: claude wrote the debugger commands, I executed them on the device:
# Monitor NSData creation for 23-byte challengesbr set -n "+[NSData dataWithBytes:length:]"br condition 6 '$arg3 == 23'br command add 6echo "🎯 23-BYTE CHALLENGE DETECTED!"x/23bx $arg2echo "Mystery byte at position 7:"x/1bx $arg2+7continueDONEI’d attach LLDB, trigger a pairing attempt, and paste back the register dumps. claude would analyze the output and write the next set of commands. Neither of us could have done this alone: claude couldn’t run the debugger, I didn’t know what to look for.
Two Logs and a Hint
With data extraction working (finally), I wanted to test whether claude could analyze captured data. Not just parse it, but figure out what it meant.
The camera has a GPS sync feature. When enabled, the phone sends its location to the camera periodically so photos can be geotagged. Somewhere in the Bluetooth traffic, there had to be coordinate data.
I gave claude two log files:
- One captured with GPS sync disabled
- One captured with GPS sync enabled
The prompt was simple:
“Compare these two logs. The delta should be the GPS sync data. Figure out the encoding.”
I didn’t tell claude which characteristic to look at. Didn’t give hints about the data format. Just: here are two logs, find the difference, decode it.
The Coords Checked Out
claude ran a differential analysis. One characteristic appeared in the GPS-enabled log but not the other: 0F36EC14-29E5-411A-A1B6-64EE8383F090. That was the GPS channel.
The raw payloads looked like this:
66 e6 47 16 ef 8e 4c b7 0c 00 00 00 00 00 00 00 ...claude started decoding the 23-byte payloads. The first 8 bytes changed between messages. claude identified them as two 4-byte values, little-endian.
First attempt: treat them as unsigned integers, divide by various powers of 10. The results didn’t look like coordinates. Values over 400 degrees, which doesn’t make sense for longitude.
Second attempt: what if one of them is signed? Western Hemisphere longitudes are negative. If the second value is a signed 32-bit integer…
Raw bytes: 0x3044eef2 (little-endian)As UInt32: 4,075,701,234 → 407.57° (invalid)As Int32: -219,266,000 → -21.93° (plausible longitude)Then the scaling factor. claude reasoned about GPS precision. Seven decimal places gives roughly 1-meter accuracy. Divide by 10,000,000.
641,417,000 ÷ 10,000,000 = 64.1417° (latitude)-219,266,000 ÷ 10,000,000 = -21.9266° (longitude)Reykjavik, Iceland: 64.14°N, 21.93°W.
I plugged the decoded coordinates into Google Maps. The pin dropped exactly where expected.
claude documented the final format:
23-Byte GPS Heartbeat Structure:Bytes 0-3: Latitude (unsigned int ÷ 10,000,000 = degrees N)Bytes 4-7: Longitude (SIGNED int ÷ 10,000,000 = degrees, W is negative)Bytes 8-15: Reserved (zeros)Bytes 16-22: TimestampByte 22: Message counter (increments each heartbeat)That was satisfying. I’d given claude raw hex bytes and a vague hint. It found the right characteristic, figured out signed vs unsigned encoding, derived the scaling factor, and validated the result against a known location. All on its own.
What the GPS Proved
The GPS decoding mattered less for the feature itself (GPS sync wasn’t the goal) and more for what it revealed about claude’s capabilities.
The NSData struggle showed a pattern: claude trying the same thing nine ways, expecting different results. The definition of insanity, automated. The GPS decoding showed the flip side: given clear problem framing and the right data, claude could genuinely solve puzzles. Find the pattern, do the math, test the hypothesis.
The difference wasn’t analysis vs implementation. claude handled both fine. The difference was oversight. Left alone too long, claude would spin. With someone watching who could say “step back, look at what you actually have,” it unstuck quickly.
Think of it like a very smart but inexperienced junior. Capable of real work, but benefits from a tech lead who knows when to intervene.