Repository navigation
Releases: Enkhoder/Spicebag
Release list
v4.0.1
A patch release: the help page stays open when a modifier key is pressed on its own, the ASCII banner's version glows on hover, and the Windows executable carries the Spicebag icon. Images and the image format are unchanged from 4.0.0.
pip install --upgrade spicebagFixes
- Help page and modifier keys. Pressing Ctrl, Shift, Alt or Super alone no longer closes the help page. Terminals that speak the kitty keyboard protocol, such as Alacritty 0.13 and later, report a bare modifier press as a key of its own, and the help page took it as the key that closes it. Every other key still closes it.
TUI app
- Hovering the version under the ASCII banner sweeps it with the inverted brand gradient, white italic text over a highlight one cell wider on each side, the way the mascot banner's title and the help page's author name already respond.
Windows executable
spicebag.execarries the Spicebag icon at 16, 32, 48, 64, 128 and 256 pixels.
Documentation
README.md: installing with pipx throughpython -m pipx, so plainspicebagworks even when neitherspicebagnorpipxis on PATH.- Proof-of-phrase is hyphenated throughout
README.md,examples/README.mdanddocs/ARCHITECTURE.md.
v4.0.0
Every image now proves its own seed phrase, so a wrong salt is rejected instead of quietly decoding to a different valid phrase. Spicebag also runs straight from the terminal: spicebag --encode, --decode and --palette work through plain prompts, with native masking for the seed phrase and salt. The TUI app itself gains a salt confirmation step and stops dragging the history to the bottom while a progress bar runs.
pip install --upgrade spicebagBreaking changes
4.0.0 changes the image format, and images made by 3.x no longer decode. They are not lost: they still decode on 3.x, so keep
pip install "spicebag<4"in a separate virtual environment for as long as you rely on them, and re-encode them here.
- Proof-of-phrase. Until now a wrong salt was only caught by the seed phrase's own checksum, which a random 12-word phrase passes about one time in fifteen. A mistyped salt, even a single stray keystroke, could decode an image to a different phrase that looked perfectly valid. Each cell now carries 3 bits of an HMAC-SHA256 over the whole phrase, keyed by a third HKDF subkey of the salt, in the top of its random block offset. The decoder recomputes it and rejects any mismatch: a wrong salt now slips through a 12-word image about one time in 10¹², and far less often for longer phrases.
- Clearer decode errors. A wrong salt reads
Salt is incorrect., a salted image decoded without its salt readsImage is salted or predates v4.0.0., since an image made before 4.0.0 fails the same way, and an unsalted image decoded with a salt readsSalt is incorrect.instead of sometimes producing a phrase: the decoder handles it exactly like a wrong salt, so it never reveals that an image is unsalted. - Altered images are caught. Swapping two cells, even two holding the same word, or changing a cell's word breaks the proof. Mixing cells between images of the same phrase and salt still decodes, since they carry the same proof.
- Fewer free colors per cell. The proof takes 3 of each offset's bits, so once a phrase and salt are fixed a cell can take 1,024 colors (2,048 for SLIP39) instead of 8,192 (16,384). A 12-word phrase still has about 1.3×10³⁶ distinct images per salt. Manual decoding of unsalted images is unchanged, because the proof lives in bits it already ignores.
- Salts are normalized to Unicode NFC before key derivation, so an accented salt typed as one composed character or as a letter plus a combining mark is the same salt. A salt made only of such decomposed characters encodes differently than it did on 3.x.
CLI mode
spicebag --encode/-easks for the image count, seed phrase, salt, salt confirmation, cell size and save path, then writes the image or ZIP and prints its filename as a clickable link to the file.spicebag --decode/-dasks for the image path and salt, reports the recovered seed phrase's length and standard, then asks whether to copy it to the clipboard. The phrase is never printed, and answeringNchecks an image and salt without exposing the phrase anywhere. If the clipboard cannot be written, the run fails rather than showing it.spicebag --palette/-pasks for the seed phrase, salt and cells, then offers to copy every color each chosen cell can take, sorted ascending underCell [row][col]headers counted from 0. Cells are row and column pairs, each index a number or a range like0-2or3-1, with*or a blank answer for every cell. The list identifies the phrase as surely as the phrase itself, so it is never printed and goes through the same one-paste clipboard as decode.- The seed phrase, the salt and its confirmation are read with native terminal masking, so nothing appears while typing.
- A rejected answer repeats the prompt with a short reason in front of it, such as
Invalid word count,Checksum failed,Salt does not match,Dir not found,PNG onlyorDirty cells. Three mismatched salt confirmations start the salt over. The image is fully validated before the salt is asked for. - Encode and decode end with
Proceed? (y/n).yoryescontinues, in any case.nornoends the run without writing anything, as do three answers that are neither, a plain ENTER included. Every abort printsOperation aborted.A blank answer where none is accepted is asked again without a message, and the third in a row ends the run. - Typed answers keep the terminal's default color, like any native prompt. Errors use its bright red, and the progress bar steps through the brand gradient while encoding and its inverse while decoding, in 256 colors, with a live stopwatch. In a window narrower than the line, the bar wraps at full length and each redraw still replaces the last.
- Pasting into any prompt clears the clipboard once the answer is read, so a pasted seed phrase or salt does not stay behind.
- Ctrl+C aborts at any step with exit status 130. An encode interrupted mid-write removes its unfinished file.
- Save paths follow the TUI app's rules, including the
//separator, and a leading~is the home folder in every save and image path, in the TUI app too. - On Windows the clipboard copy is kept out of clipboard history and cloud sync. macOS uses
pbcopyand Linux useswl-copy. Under WSL, whose clipboard mirror would use up the one paste, decode and palette reportClipboard unavailable for WSL.instead of asking to copy.
TUI app
- Double ESC exits. At the main menu, ESC shows
Press ESC again to exit, and a second ESC within a second closes Spicebag. An ESC less than half a second after the previous one never arms it, so a burst of ESC that steps back out of a flow or closes the help screen cannot quit by accident. - Limited/legacy terminal notice. In the classic Windows console host, or in a terminal that does not advertise truecolor, the header names the terminal and its version and recommends CLI mode instead.
- No word count step. Encode goes straight from the image count to the seed phrase, whose length decides the standard. The status bar counts words as
Word n, typing and pasting stop at 33 words, and a phrase of an unsupported length readsInvalid word count., stacked in front of the masked words asInvalid word count, invalid seed words: ···when some are not in any wordlist. - Image salt is confirmed. After entering an image salt, encode asks to confirm it, masked like the first entry and with the same status bar. A mismatch reads
Salt does not match.and asks again. This happens with no salt as well, where confirming means pressing ENTER again. - Progress no longer scrolls the screen. While an encode or decode runs, scrolling up through the history stays put. The view jumps to the bottom only when the run reaches 100%, fails, or is stopped with
cancel. - Resizing keeps your place. Changing the window size no longer jumps to the bottom unless the view was already there, and the ASCII art banner keeps its shape in a narrow window, running off the right edge instead of wrapping into broken rows.
- Paths are shown in the history. The save path and the image path are logged as typed, minus surrounding spaces, with ENTER on the save path logging the default folder, and screenshots no longer blank them. The seed phrase and salts stay masked.
- Pasting clears the clipboard. Any paste into the TUI app empties the system clipboard, so a pasted seed phrase or salt does not stay behind.
- Default filenames no longer describe their contents. A single image saves as
20260930_123456.pngand a bulk archive asx1000_20260930_123456.zip, instead of names that announced a seed image and its word count. The output folders are now~/Spicebag/imagesand~/Spicebag/screenshots, renamed fromencoded-imagesandapp-screenshots. Files already in the old folders stay there. - Minor UX and quality-of-life updates and fixes.
Flags
spicebag --helplists--encode,--decodeand--paletteand adds short Save paths, Decode paths and Palette cells sections.python -m spicebagruns Spicebag when thespicebagcommand is not on PATH.- A mistyped flag gets a suggestion:
spicebag --encofeprintsDid you mean --encode?, and a barespicebag encodepoints at--encodetoo. Nothing runs until the flag is typed correctly. The error no longer lists the two help invocations after it.
Installation
- Every GitHub Release from 4.0.0 on carries a standalone
spicebag.exefor Windows that needs no Python. It is not code-signed, so SmartScreen may warn on first launch. - A source checkout runs with
python -m spicebag, and so doesrun.bat, soPYTHONPATHno longer needs setting. - Python 3.14 is supported.
Privacy hardening
- A crash prints its traceback without the values of local variables, which could hold the seed phrase or salt. Textual's crash output showed them, and so does Typer's on versions that default to it.
- Every
TEXTUAL*environment variable is cleared at startup.TEXTUAL_LOGmade Textual write every keystroke and input change to a file, andTEXTUAL_SCREENSHOTsaved an unmasked screenshot.
Examples and tests
- Every decodable image in
examples/images/is re-encoded in the new format from the same phrases and salts, and24-interchanged-seed-Enkhoder.pngis rebuilt from cells of two images of one phrase and salt. Previous-format-3.0-undecodable.pngkeeps a 3.0 image as a regression case that must be rejected.tests/smoke.pychecks that wrong salts, a missing salt and a superfluous salt are all rejected, and round-trips 15, 18 and 21-word phrases, which no fixture covers.
Platform testing
The TUI app and CLI mode are verified on Windows, and under WSL on Ubuntu, Fedora and Arch. WSL exercises neither the Linux clipboard nor the Linux network detection. The CLI mode c...
v3.0.0
Image encoding starts over. An unsalted image now stores every word as a literal chunk of the 24-bit color space, so a seed phrase can be read back from its PNG with a color picker and a wordlist, without Spicebag installed.
pip install --upgrade spicebagBreaking changes
3.0.0 is a fresh start for images. Every image encoded or decoded from this version on follows the new color rules, and images made by 1.x or 2.x do not decode here. Those images are not lost: they still decode on 2.x, so keep
pip install "spicebag<3"in a separate virtual environment for as long as you rely on them.
- Unsalted words map onto literal hex chunks. The 16,777,216 colors are split in order into one chunk per word, and a word's color always falls inside its own chunk. Word 1 of a 2048-word list (abandon) owns
#000000to#001FFF, word 2 owns#002000to#003FFF, and the last word owns#FFE000to#FFFFFF. The 1024-word SLIP39 list uses chunks of 16,384. Cells still run left to right, top to bottom. RGB_VALUE_SHIFTSandPERMUTATIONSare removed. The fixed per-position channel shifts and the six channel permutations sat between the word index and the color, hiding the chunk structure.encodeWordanddecodeColortakecolorTablesin place ofshiftandmask, andColorSpacecarriescolorTablesin place ofshiftsandmasks.- Salted colors scatter across the whole color space. With a salt, the 24-bit value passes through a salt-keyed permutation before it becomes the color: a 10-round Feistel network whose round keys come from the Argon2id master key through HKDF. Each word still owns exactly 8,192 colors (16,384 for SLIP39) and no color belongs to two words, but those colors are spread pseudo-randomly over all 16,777,216 instead of sitting in one slice, so rerolling a salted cell can land on any hue. The per-position XOR mask is gone: within one salt a word keeps the same color set in every cell, and the salt-seeded cell shuffle still hides word positions. The number of colors per word is unchanged, so every phrase and salt still has as many distinct images as before.
Manual decoding
The README has a new Manual Decoding section that walks through reading an unsalted image by hand: find the wordlist from the aspect ratio, read each cell's hex code, and compute the word number from red and green alone (R × 8 + ⌊G ÷ 32⌋ + 1, or R × 4 + ⌊G ÷ 64⌋ + 1 for SLIP39). An unsalted image no longer depends on Spicebag, or on any software at all, still being around to read it.
Interface
- The help screen explains the color chunks under HOW THIS WORKS, with abandon and ability as worked ranges, and labels the ENCODE image sample as unsalted: rerolling a cell keeps its color inside the same word's chunk. Its salting paragraph explains that a salt scatters each word's colors across the whole color space.
- An unknown command in the main menu reads
No such command: <input>.
Command line
spicebag --helpandspicebag -hprint a compact usage block with no blank lines.- Any argument that is not one of the four flags stops the run before the interface starts. It prints
No such option: <input>for a token starting with-andNo such command: <input>for anything else, followed byspicebag --helpandspicebag -h, and exits with status 2. Whole tokens are checked, so a stray word after-his an error instead of being ignored, and combined short flags such as-hVare rejected.
Performance and memory
- Cell integrity is checked cell by cell instead of pixel by pixel. Decoding asks Pillow for the colors inside a cell and stops the moment a second one appears, rather than walking every pixel in Python. A 2000px-per-cell image is checked in under a second instead of a couple of minutes, and the same files are accepted and rejected as before.
- Finished output is frozen. A command that has finished can no longer change, so the main menu draws it once and keeps the picture, redrawing it only when the window gets narrower. That is why encoding and decoding now run at the same speed in a long session as in a fresh one: with fifty commands of history, a redraw takes 0.44 ms instead of 239 ms.
- Masked output is dropped from memory. Masking used to be a picture drawn over words the program still held. Now, when a command finishes, its decoded words, invalid word notes and image sample colors are released along with the rest of the block, and only the masked picture is kept.
- The installed version string is read from the package metadata once per session rather than on every mouse move.
Examples and tests
- Every decodable image in
examples/images/is re-encoded from the same phrases and salts, sotests/smoke.pyand the release job's decode check both run against the new format. - The images kept as rejection cases are named for what they test:
Corrupted-png-compressed-pixels.pngfor cells that are no longer one flat color,Corrupted-png-invalid-aspect-ratio.pngfor a shape that matches no grid, andCorrupted-png-invalid-color-space.pngfor an alpha channel that is not fully opaque.
Platform testing
CI runs the encode and decode round trip on every change on Linux (Python 3.10 and 3.13), Windows (3.12) and macOS (3.12), so the new color transform is covered on all three platforms. The interface is verified on Windows only. The per-OS code that 2.0.0 disclosed as unverified is unchanged and still has not run on real macOS or Linux hardware: the POSIX half of the palette probe in utils/terminalColors.py, the OSC 0 window-title fallback in app/cli.py, and the Linux and macOS network detection in utils/networkDetect.py. Each degrades to a safe default rather than raising. Reports from macOS and Linux users close that gap fastest: open an issue.
Documentation
README.md: the Manual Decoding section, a Security Considerations warning that an unsalted image protects nothing, How It Works and Configuration Space rewritten around the two-step color transform, and the banner mascot's belt split between the brand gradient and its inversiondocs/ARCHITECTURE.md: the color transform with its salted Feistel permutation, why the unsalted path takes no transform after the block offset, the command line's input check, how both screens draw through the Strip based log, and the invariant that a finished block is frozen and its nodes releasedSECURITY.md: reading an unsalted image by hand is listed as out of scope, since it is intentional by design
v2.0.0
The interface is now the only way in. The encode and decode subcommands are gone, screenshots render against your real terminal colors instead of a fixed palette, and the in-app guide redraws from cached strips rather than rewriting itself.
pip install --upgrade spicebagBreaking changes
spicebag encodeandspicebag decodeare removed. Encoding and decoding happen in the TUI and nowhere else. A seed phrase passed as a command-line argument lands in shell history, in the process table, and in any shell integration that records commands. The application can clear none of that afterwards. Scripts that called either subcommand will need rewriting; there is no drop-in replacement, by design.- The command line now accepts flags only:
--version/-Vand--help/-h. - Everything that shelled out to those subcommands was rewritten to match: CI smoke-tests
spicebag --help, the release job decodes the example images throughdecodeImagedirectly, andexamples/README.mdwalks through the interface instead.
Screenshots match your terminal
Exports used to be re-rendered through Rich's SVG_EXPORT_THEME and Textual's MONOKAI, so every capture came out with a #292929 frame and a #0C0C0C body regardless of how the terminal was configured. Spicebag now queries the terminal for its real background, foreground, and all sixteen ANSI slots (OSC 11, OSC 10, OSC 4) at startup and renders the SVG against those.
Terminals that do not answer fall back to the previous fixed dark theme, so nothing regresses. On Windows, legacy conhost.exe answers no queries and is read from its console color table instead.
Interface
- Screenshots are bound to F12. The previous Ctrl+S is intercepted as XOFF by legacy console hosts, which made the security warning screen impossible to capture.
- The window title is set through the Win32 console API on Windows, so legacy hosts no longer print the raw escape sequence as a stray line at startup.
--helpis hand-written rather than generated, and no longer advertises framework defaults.- The help screen renders from cached strips instead of rewriting the whole log on every frame. It lists F12 alongside the typed commands, and Ctrl-clicking the author credit in its footer opens the GitHub profile.
Platform testing
The encoding core is covered on all three platforms. The interface is verified on Windows only.
CI runs the encode and decode round trip on Linux, Windows and macOS for every change, so the cryptography and the PNG path are exercised everywhere. What CI cannot drive is a full-screen interface, and what it never touches is the per-OS probing code. Nothing below is known to be broken; treat it as unverified rather than unsupported.
| OS | Status |
|---|---|
| Windows 11 | Fully exercised: interface, screenshots, Windows Terminal and legacy conhost.exe |
| macOS | Core green in CI; interface exercised only in a VMware guest, never on real hardware |
| Linux | Core green in CI; interface not exercised |
The code that branches on platform, and therefore carries the most risk:
utils/terminalColors.py: the POSIX half of the palette probe (termios,tty,select) has never run. The Windows half and the fallback theme are covered.app/cli.py:setTerminalTitlefalls back to an OSC 0 escape sequence off Windows.utils/networkDetect.py: the Linux/sys/class/netandrfkillreads, and the macOSnetworksetupandifconfigshell-outs plus the Bluetooth plist read, are unverified.
Each of these degrades to a safe default rather than raising, so a failure should cost a single feature rather than the session. Reports from macOS and Linux users are the fastest way to close the gap: open an issue.
Supply chain
Publishing still runs through PyPI Trusted Publishing. No long-lived API token exists on any machine or in any repository secret. release.yml fires on a v* tag, refuses to build unless the tag matches the version in pyproject.toml, builds and twine checks the wheel and sdist, then holds publishing behind the pypi environment for manual approval.
Before that gate it installs the freshly built wheel into a clean virtualenv and decodes two example images. That step used to call the decode subcommand this release removes, so it now calls decodeImage directly, and still proves a clean install can decode a real PNG.
ci.yml runs the encode/decode round trip in tests/smoke.py across Linux, Windows and macOS on Python 3.10 through 3.13, and fails the build if the wheel ever contains a file that must not ship.
Documentation
README.md: usage, the encoding algorithm, the configuration-space math, and a new terminal-compatibility section covering 24-bit color, mouse reporting, OSC 8 hyperlinks, and palette queries per OSdocs/ARCHITECTURE.md: module map and the invariants that aren't visible from any single fileSECURITY.md: threat model and scope
v1.0.0
Spicebag v1.0.0
Encode a cryptocurrency wallet seed phrase into a color-coded PNG, and decode it back. Each word maps to one RGB cell in a grid sized to the word count; an optional salt runs through Argon2id and HKDF to add XOR masking and shuffle the cell layout, so the same phrase produces an unreadable image without the salt.
pip install spicebagStandards
| Standard | Word counts | Wordlist |
|---|---|---|
| BIP-39 | 12, 15, 18, 21, 24 | 2048 words |
| Electrum | 12, 24 | 2048 words |
| SLIP-39 | 20, 33 | 1024 words |
Interfaces
- TUI: run
spicebagwith no arguments for the full interactive flow: encode, decode, bulk-generate a ZIP of variants, or browse the in-app help screen. - CLI:
spicebag encode "<phrase>" <path> [--salt <s>] [--cell-px <n>]andspicebag decode <path> [--salt <s>]for scripting.
Security
- Salt is processed with Argon2id (
time_cost=4,memory_cost=256 MB); the salt itself is never stored anywhere. - The color offset — the only free variable in the encoding — is drawn from
secrets.SystemRandom(), so two encodes of the same phrase and salt never produce the same image. - A seed phrase or salt never reaches the on-screen log, the status bar, or an exported screenshot.
- PNG decoding validates chunk types and per-cell color consistency before trusting any pixel data.
- Full threat model and scope:
SECURITY.md.
Supply chain
Published via PyPI Trusted Publishing — no long-lived API token exists on any machine or in any repository secret. Every release builds from a tagged commit through release.yml, runs tests/smoke.py and a clean-install check across the build matrix, and requires manual approval before publishing.
Documentation
README.md: usage, the encoding algorithm, and the configuration-space mathdocs/ARCHITECTURE.md: module map and the invariants that aren't visible from any single file, for anyone extending the project