How to work on this project. Read this before making changes.
Install with the open skills CLI (global, non-interactive — available in every Claude Code session):
npx skills add yagame672-blip/stegstr2 --skill "skill" -g -a claude-code -yOr manually — copy the SKILL.md below into:
~/.claude/skills/skill/SKILL.md# Stegstr 2 SKILL
How to work on this project. Read this before making changes.
## Layout
- `stegstr2/` — the Python package.
- `carriers/` — per-format embed/extract: `png_dwt` (PNG, Haar DWT / S-transform),
`jpeg_qim` (JPEG, DCT-QIM), `gif_lsb` (GIF, palette LSB). Each exposes
`embed(image_bytes, frame, **opts)`, `extract(image_bytes, **opts)`,
`capacity(image_bytes, **opts)`.
- `frame.py` — payload framing. Reference lossless frame is
`"STEGSTR" | len(4 BE) | payload`. Robust framing is the same frame wrapped in
concatenated RS(255,170) codewords (`rs_encode`/`rs_decode`). Legacy `STEGSTR2`
v2 frames are still parsed for backward compatibility.
- `stego.py` — orchestration: `embed_payload` / `detect_payload` / `capacity_for`.
Encryption is payload-prefix based: open mode = `STEGSTR1` AES-GCM; recipients =
NIP-04 JSON envelope wrapped by the open app-encryption.
- `crypto.py` — AES-GCM open encryption, NIP-04 ECDH (secp256k1 via `ecdsa`).
- `nostr.py` — pure-Python BIP-340 Schnorr, NIP-01 event ids, bech32 (nsec/npub).
- `bundle.py` — the embedded JSON bundle (`{version, events}`) + post/DM helpers.
- `store.py` — SQLite store (thread-safe; `STEGSTR2_HOME` env override).
- `net/` — Nostr relay client (`relay.py`), sync (`sync.py`), watch (`watch.py`).
- `cli.py` — `python -m stegstr2.cli ...` (init, genkey, embed, detect, post, ...).
- `mcp_server.py` — MCP stdio server (JSON-RPC 2.0, line-delimited).
- `web/` — stdlib HTTP UI + REST API (`server.py`, `static/`).
- `tools/` — `channel_sim.py` (channel profiles) and `benchmark.py`
(`python -m tools.benchmark --stress`).
- `tests/` — pytest suite. Run with `python -m pytest`.
- `schema/bundle.schema.json` — JSON Schema for the bundle.
## Conventions
- No code comments unless they explain *why*.
- Domain errors live in `errors.py` (`CarrierError`, `DecodeError`, `CryptoError`,
`PayloadTooLargeError`, `RelayError`).
- Carrier embed returns bytes (re-encoded image); extract returns raw stream bytes;
framing/decryption is the caller's job (stego.py).
- Robust frames are used for JPEG, GIF and PNG carriers.
- `parse_stream` tries robust (RS codeword boundaries) then lossless (magic scan).
## Hard-won facts
- The JPEG DCT basis `_T` is NOT symmetric: forward `T·B·T^T`, inverse `T^T·D·T`
(always use explicit `._T.T`).
- QIM nudge condition is `apply = target * diff < strength`, not `need > 0`.
- Value-threshold ("valid mask") embedding is impossible to keep consistent under
±1 nudges — never reintroduce it. Use no-mask LH-LSB + RS framing.
- JPEG QIM survives recompression but NOT non-integer downscaling (block misalignment
→ ~50% bit flips). Cover images should be sized to the target channel's max
dimension before embedding; `tools/benchmark.py --stress` documents this limit.
- The system `python` on PATH is NOT the project venv — always use
`.venv/Scripts/python.exe`.
- The environment can duplicate/mangle captured stdout. Verify with exit codes,
sentinel strings, or JSON files.
## Verification
- `python -m pytest` in the project root.
- `python -m tools.benchmark` → expect `BENCH_OK` and exit 0.
- Quick smoke: `python -m stegstr2.cli init`, `embed`, `detect`.