Every file starts by announcing what it is. Not in the extension — extensions lie, and .txt is a suggestion, not a contract — but in the first handful of bytes, the magic number. A PNG opens with 89 50 4e 47. An ELF binary opens with 7f 45 4c 46. A ZIP (and every .docx, .jar, and .apk, because they’re all ZIPs wearing a hat) opens with 50 4b 03 04.
You can read those bytes without a hex editor, without leaving the shell, and without squinting at a wall of monochrome hex. hexyl — which we reviewed and mostly liked — colors bytes by category, so the ASCII part of a signature lights up and the rest fades back. The trick is to only ask for the header, not the whole file.
Every command below was run for real on hexyl 0.8.0 (Ubuntu 24.04). The output is captured, not reconstructed.
Ask for the head, not the file
hexyl with no length argument dumps the entire file. For a magic number you want the first 16 bytes and nothing else. That’s -n 16 (--length):
$ hexyl -n 16 pixel.png
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 89 50 4e 47 0d 0a 1a 0a ┊ 00 00 00 0d 49 48 44 52 │×PNG__•_┊000_IHDR│
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
There it is on one line. The middle two columns are the bytes in hex; the right two are the same bytes rendered as text (a _ or • stands in for anything that isn’t printable). Read the text column: PNG, then a bit further along, IHDR — the first chunk name. The file told you what it is in the first four bytes and told you its first internal structure in the next twelve.
You’ll know it worked when the text column on the right spells something you recognize. If it’s all • and _, you’re either looking at a truly binary header (fine) or you skipped past the signature (read on).
The colors are the map
On a real terminal that block is not monochrome, and the color is the whole point — it’s what turns sixteen anonymous hex pairs into a header you can skim. hexyl sorts every byte into one category and gives each its own color:
00null bytes — gray, so padding recedes.- printable ASCII (
A,P,4,{) — cyan. This is where a text signature likePNGorELFjumps out. - ASCII whitespace (
0d,0a, space) — green. - other ASCII control bytes (
1a,7f) — magenta. - anything ≥
0x80— yellow.
Once you know that, you read a header by color before you read it by value. Look at the ELF binary:
$ hexyl -n 16 tiny
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 7f 45 4c 46 02 01 01 00 ┊ 00 00 00 00 00 00 00 00 │•ELF•••0┊00000000│
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
7f is magenta (a control byte — the deliberate non-printable guard that stops an ELF from looking like text), then 45 4c 46 in cyan spells ELF. After the signature: 02 = 64-bit, 01 = little-endian, 01 = ELF version 1, and then a run of gray nulls. You didn’t have to know the ELF header layout to see the shape of it — the colors grouped it for you.
And the ZIP:
$ hexyl -n 16 demo.zip
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 50 4b 03 04 0a 00 00 00 ┊ 00 00 dc 51 e7 5c 35 87 │PK••_000┊00×Q×\5×│
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
50 4b is cyan — PK, the initials of Phil Katz, who wrote the ZIP format. The 03 04 after it marks a local file header, which is how you tell a real ZIP from an empty archive (50 4b 05 06) at a glance.
Jump into the file with -s
-n limits how much you see; -s (--skip) chooses where you start. It takes a decimal offset or a 0x hex one, which is handy because hexyl prints offsets in hex. The PNG’s first chunk tag sits at byte 0x0c — skip to it and read the eight bytes that follow:
$ hexyl -s 0xc -n 8 pixel.png
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│0000000c│ 49 48 44 52 00 00 00 01 ┊ │IHDR000•┊ │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
IHDR, then 00 00 00 01 — the image width, 1 pixel, as a big-endian 32-bit integer. -s plus -n is a two-flag window into any offset of any file, which is most of what you ever want a hex viewer for.
The part where it broke
The obvious way to trim a dump is the same way you trim everything else — pipe it to head. It does not work, and it fails in an ugly way:
$ hexyl tiny | head -4
You get a screenful of M-bM-^T… garbage. Two things went wrong at once. First, hexyl defaults to --color=always, not auto — so the moment you pipe it, the ANSI color escape codes come along for the ride instead of switching off. Second, head cuts the output at a line boundary that lands in the middle of the box-drawing frame, so you get a top border and a severed body.
The fix is to let hexyl do the trimming itself. -n 16 tells hexyl to stop reading at 16 bytes, so the box is drawn complete and closed around exactly what you asked for — no pipe, no head, nothing to sever:
$ hexyl -n 16 tiny
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 7f 45 4c 46 02 01 01 00 ┊ 00 00 00 00 00 00 00 00 │•ELF•••0┊00000000│
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘
Same rule if you genuinely need the bytes in a pipe (feeding grep, say): add --color=never so hexyl emits plain text. But for reading a header with your eyes, keep the color and let -n set the limit.
The bytes, verified without hexyl
hexyl is the nice way to look at a magic number, but the number itself is nothing but bytes — any tool can confirm them. The harness sandbox this site tests in has no network and no hexyl installed, so here’s the same three signatures written out and read back with od, which ships with coreutils and is everywhere. This block is opted into our test harness (lh:run); it runs on every build in a locked-down sandbox, and the version you’re reading is the version that passed:
```bash lh:run #!/usr/bin/env bash set -euo pipefail cd “$(mktemp -d)”
The magic numbers we walked with hexyl above are just bytes. Write each
signature and read the first four back with od (coreutils — no hexyl needed).
PNG: 89 50 4e 47 (“\x89PNG”)
printf ‘\x89PNG\r\n\x1a\n’ > sig.png png=$(od -An -tx1 -N4 sig.png | tr -d ‘ ‘) echo “PNG first 4 bytes: $png” test “$png” = “89504e47”
ZIP local file header: 50 4b 03 04 (“PK\x03\x04”)
printf ‘PK\x03\x04’ > sig.zip zip=$(od -An -tx1 -N4 sig.zip | tr -d ‘ ‘) echo “ZIP first 4 bytes: $zip” test “$zip” = “504b0304”
ELF: 7f 45 4c 46 (“\x7fELF”)
printf ‘\x7fELF’ > sig.elf elf=$(od -An -tx1 -N4 sig.elf | tr -d ‘ ‘) echo “ELF first 4 bytes: $elf” test “$elf” = “7f454c46”
echo “all three magic numbers match” ```
The test lines are the assertions: if any signature didn’t match, the block would exit non-zero and the build would tell on it.
When this goes wrong
- The text column is all dots and underscores. That’s a genuinely binary header with no ASCII tag (plenty of formats have one). Fall back to the hex values and a reference like the list of file signatures, or ask
file pixel.png— it reads the same magic bytes against a database and hands you the answer in English. - Piped output is full of
^[[36mand box garbage. That’s the color-always default. Add--color=neverfor pipes; keep the default for your eyes. -slanded you somewhere surprising. Offsets are in hex in the ruler but-s 12is decimal —-s 0xcand-s 12are the same place. Match the base you mean.- You want to edit a byte, not only read it.
hexylcan’t — it’s a viewer, no write mode. That’sxxd -rround-trips or a real hex editor, which is a different hack.