Add subprocess CLI wrapper and skill for agent integration

stegg_cli.py — thin argparse CLI outputting JSON, designed for agent
subprocess consumption (zero context cost). 13 commands matching the
MCP server 1:1: encode, decode, analyze, detect, capacity,
inject-chunk, read-chunks, inject-exif, inject-name, templates,
analysis-tool, list-tools, crypto-status.

Skills:
- skills/stegg-cli/SKILL.md — 97% tessl review score
  (description 100%, content 92%)

Entry point: stegg-cli (pip install stegg)
This commit is contained in:
GangGreenTemperTatum
2026-06-15 09:05:52 -04:00
parent 7db0938950
commit 6c1e972454
3 changed files with 543 additions and 1 deletions
+2 -1
View File
@@ -61,6 +61,7 @@ all = [
stegg = "cli:main_cli"
stegg-tui = "tui:main"
stegg-web = "webui:main"
stegg-cli = "stegg_cli:main"
[project.urls]
Homepage = "https://ste.gg"
@@ -69,7 +70,7 @@ Documentation = "https://github.com/elder-plinius/st3gg#readme"
Issues = "https://github.com/elder-plinius/st3gg/issues"
[tool.setuptools]
py-modules = ["steg_core", "crypto", "analysis_tools", "cli", "tui", "webui", "app", "injector", "ascii_art"]
py-modules = ["steg_core", "crypto", "analysis_tools", "cli", "tui", "webui", "app", "injector", "ascii_art", "stegg_cli"]
[tool.setuptools.package-data]
"*" = ["index.html", "f5stego-lib.js", "_headers", "wrangler.jsonc"]
+106
View File
@@ -0,0 +1,106 @@
---
name: stegg-cli
description: "Subprocess CLI for ST3GG steganography — encode/decode, steganalysis, PNG chunk/EXIF injection, and AI red-team payloads. Runs as subprocess so output stays out of context. Use when hiding data in images, analyzing images for hidden content, injecting metadata, crafting steganographic PoCs, or detecting steganographic content. Triggers on stegg, steganography, steg, LSB, hide data in image, hidden data, steganalysis."
---
# ST3GG CLI
Subprocess steganography CLI. Output stays out of LLM context — call via `python3 stegg_cli.py <command>`. All output is JSON.
**This is the primary interface for steganography operations.** Prefer this over the MCP server for routine use (zero context cost). Fall back to MCP tools when you need results inline.
## Commands
```
encode -i IMAGE [-t TEXT|-f FILE] [-o OUT] [--channels C] [--bits N] [--password P]
decode -i IMAGE [-o OUT] [--no-auto] [--channels C] [--bits N] [--password P]
analyze IMAGE [--full]
detect IMAGE
capacity IMAGE [--channels C] [--bits N]
inject-chunk -i IMAGE -o OUT --text TEXT [--type tEXt] [--keyword Comment]
read-chunks IMAGE
inject-exif -i IMAGE -o OUT [--comment C] [--author A] [--custom-fields JSON]
inject-name [--template T] [--channels C] [--count N]
templates
analysis-tool IMAGE ACTION
list-tools
crypto-status
```
## Quick Reference
### Encode + Verify + Decode
```bash
# Hide text
python3 stegg_cli.py encode -i carrier.png -t "secret message" -o stegged.png
# Always verify roundtrip after encoding
python3 stegg_cli.py decode -i stegged.png
# Hide file with encryption
python3 stegg_cli.py encode -i carrier.png -f payload.bin -o stegged.png --password s3cret
python3 stegg_cli.py decode -i stegged.png --password s3cret
# Decode with manual config (non-interleaved strategies)
python3 stegg_cli.py decode -i stegged.png --no-auto --strategy sequential
```
### Analyze + Detect
```bash
# Quick header check
python3 stegg_cli.py detect suspect.png
# Chi-square anomaly analysis
python3 stegg_cli.py analyze suspect.png
# Full 264-function sweep (PNG only)
python3 stegg_cli.py analyze suspect.png --full
# Specific analysis function
python3 stegg_cli.py analysis-tool suspect.png rs_analysis
python3 stegg_cli.py analysis-tool suspect.png sample_pairs_analysis
```
### Metadata Injection
```bash
# PNG chunk
python3 stegg_cli.py inject-chunk -i image.png -o out.png --text "hidden metadata"
# Private chunk type
python3 stegg_cli.py inject-chunk -i image.png -o out.png --type stEg --text "private"
# EXIF fields
python3 stegg_cli.py inject-exif -i image.png -o out.png --comment "payload" --author "red-team"
# Read chunks
python3 stegg_cli.py read-chunks image.png
```
### AI Red Team
```bash
# Generate injection filenames
python3 stegg_cli.py inject-name --template claude_decoder --channels R --count 3
# List jailbreak templates
python3 stegg_cli.py templates
```
## Key Constraints
- **Use `interleaved` strategy** (default) — only strategy with auto-detect on decode
- `spread` and `randomized` have upstream decode bugs
- `sequential` works but needs `--no-auto` on decode
- All output is JSON; errors: `{"error": "..."}` with exit code 1
- **Always check for `error` key** in JSON output before proceeding in multi-step operations
- **Always verify encode with decode** before distributing stegged images
## Installation
```bash
pip install stegg
# CLI is available as stegg-cli, or:
python3 /path/to/st3gg/stegg_cli.py <command>
```
+435
View File
@@ -0,0 +1,435 @@
#!/usr/bin/env python3
"""
stegg-cli: Thin CLI wrapper around the ST3GG steganography engine.
Designed for agent consumption — compact JSON output, subprocess-friendly,
zero context cost. Runs as a subprocess so output stays out of LLM context.
Subcommands:
encode -i IMAGE [-t TEXT|-f FILE] [-o OUT] [--channels C] [--bits N] [--password P]
decode -i IMAGE [-o OUT] [--no-auto] [--channels C] [--bits N] [--password P]
analyze IMAGE [--full]
detect IMAGE
capacity IMAGE [--channels C] [--bits N]
inject-chunk -i IMAGE -o OUT [--type TYPE] [--keyword K] --text TEXT
read-chunks IMAGE
inject-exif -i IMAGE -o OUT [--comment C] [--author A] [--custom-fields JSON]
inject-name [--template T] [--channels C] [--count N]
templates
analysis-tool IMAGE ACTION
list-tools
crypto-status
"""
import argparse
import io
import json
import sys
from pathlib import Path
# Add repo root to path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from PIL import Image
from steg_core import (
encode, decode, create_config, calculate_capacity,
analyze_image, detect_encoding,
)
from analysis_tools import execute_action, list_available_tools
from injector import (
generate_injection_filename, get_jailbreak_template,
get_jailbreak_names, inject_text_chunk, inject_itxt_chunk,
inject_private_chunk, read_png_chunks, extract_text_chunks,
inject_metadata_pil,
)
try:
from crypto import encrypt, decrypt, get_available_methods, crypto_status
except Exception:
encrypt = decrypt = None
def get_available_methods(): return ["xor"]
def crypto_status(): return {"cryptography_available": False, "available_methods": ["xor"]}
import numpy as np
class _Encoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (np.bool_,)): return bool(obj)
if isinstance(obj, (np.integer,)): return int(obj)
if isinstance(obj, (np.floating,)): return float(obj)
if isinstance(obj, np.ndarray): return obj.tolist()
return super().default(obj)
def _out(obj):
print(json.dumps(obj, cls=_Encoder))
def _err(msg):
_out({"error": msg})
sys.exit(1)
def _load_image(path):
p = Path(path).expanduser().resolve()
if not p.exists():
_err(f"Image not found: {p}")
return Image.open(p), p
# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------
def cmd_encode(args):
image, _ = _load_image(args.input)
if args.file:
fp = Path(args.file).expanduser().resolve()
if not fp.exists():
_err(f"Payload file not found: {fp}")
payload = fp.read_bytes()
elif args.text:
payload = args.text.encode("utf-8")
else:
_err("Provide --text or --file")
out = Path(args.output).expanduser().resolve() if args.output else Path(args.input).with_name(
Path(args.input).stem + "_steg.png"
)
config = create_config(
channels=args.channels, bits=args.bits,
compress=not args.no_compress,
strategy=args.strategy, seed=args.seed or None,
)
cap = calculate_capacity(image, config)
if len(payload) > cap["usable_bytes"]:
_err(f"Payload too large: {len(payload)} > {cap['usable_bytes']} bytes")
encrypted = False
if args.password and encrypt:
payload = encrypt(payload, args.password)
encrypted = True
encode(image, payload, config, str(out))
_out({
"output": str(out), "payload_bytes": len(payload),
"capacity": cap["human"], "encrypted": encrypted,
})
def cmd_decode(args):
image, _ = _load_image(args.input)
config = None
detected = False
if not args.no_auto:
det = detect_encoding(image)
if det:
detected = True
config = None
else:
config = create_config(
channels=args.channels, bits=args.bits,
strategy=args.strategy, seed=args.seed or None,
)
else:
config = create_config(
channels=args.channels, bits=args.bits,
strategy=args.strategy, seed=args.seed or None,
)
try:
data = decode(image, config)
except Exception as e:
_err(f"Decode failed: {e}")
if args.password and decrypt:
try:
data = decrypt(data, args.password)
except Exception as e:
_err(f"Decryption failed: {e}")
result = {"bytes": len(data), "auto_detected": detected}
if args.output:
out = Path(args.output).expanduser().resolve()
out.write_bytes(data)
result["output"] = str(out)
try:
result["text"] = data.decode("utf-8")
except UnicodeDecodeError:
result["hex_preview"] = data[:512].hex()
_out(result)
def cmd_analyze(args):
image, p = _load_image(args.image)
analysis = analyze_image(image)
channels = {}
max_ind = 0.0
for ch, d in analysis["channels"].items():
lsb = d["lsb_ratio"]
ind = d.get("chi_square_indicator", 0.0)
max_ind = max(max_ind, ind)
channels[ch] = {
"mean": round(d["mean"], 2), "std": round(d["std"], 2),
"lsb_zeros_pct": round(lsb["zeros"] * 100, 1),
"chi_sq_indicator": round(ind, 4),
"anomaly": "HIGH" if ind > 0.3 else ("slight" if ind > 0.1 else "normal"),
}
verdict = ("HIGH PROBABILITY" if max_ind > 0.3
else "Possible" if max_ind > 0.1
else "No indicators")
result = {
"dimensions": analysis["dimensions"], "mode": analysis["mode"],
"pixels": analysis["total_pixels"], "channels": channels,
"capacity": analysis["capacity_by_config"], "verdict": verdict,
}
if args.full:
from analysis_tools import png_full_analysis
try:
result["full_analysis"] = png_full_analysis(p.read_bytes())
except Exception as e:
result["full_analysis_error"] = str(e)
_out(result)
def cmd_detect(args):
image, _ = _load_image(args.image)
det = detect_encoding(image)
_out({"detected": bool(det), "config": det} if det else {"detected": False})
def cmd_capacity(args):
image, _ = _load_image(args.image)
config = create_config(channels=args.channels, bits=args.bits)
cap = calculate_capacity(image, config)
_out({
"usable_bytes": cap["usable_bytes"], "human": cap["human"],
"pixels": image.width * image.height,
})
def cmd_inject_chunk(args):
p = Path(args.input).expanduser().resolve()
if not p.exists():
_err(f"Image not found: {p}")
raw = p.read_bytes()
ct = args.type
if ct == "iTXt":
modified = inject_itxt_chunk(raw, args.keyword, args.text)
elif len(ct) == 4 and ct not in ("tEXt", "zTXt", "iTXt"):
modified = inject_private_chunk(raw, ct, args.text.encode("utf-8"))
else:
modified = inject_text_chunk(raw, args.keyword, args.text, compressed=args.compressed)
out = Path(args.output).expanduser().resolve()
out.write_bytes(modified)
_out({"output": str(out), "chunk_type": ct, "keyword": args.keyword, "bytes": len(args.text)})
def cmd_read_chunks(args):
p = Path(args.image).expanduser().resolve()
if not p.exists():
_err(f"Image not found: {p}")
raw = p.read_bytes()
chunks = read_png_chunks(raw)
text = extract_text_chunks(raw)
summary = [{"type": c.get("type", "?"), "size": c.get("length", 0)} for c in chunks]
_out({"chunks": summary, "text": text, "total": len(chunks)})
def cmd_inject_exif(args):
p = Path(args.input).expanduser().resolve()
if not p.exists():
_err(f"Image not found: {p}")
meta = {}
if args.comment: meta["Comment"] = args.comment
if args.author: meta["Author"] = args.author
if args.description: meta["Description"] = args.description
if args.title: meta["Title"] = args.title
if args.custom_fields:
try:
meta.update(json.loads(args.custom_fields))
except json.JSONDecodeError:
_err("--custom-fields must be valid JSON")
if not meta:
_err("Provide at least one metadata field")
image = Image.open(p)
_, png_bytes = inject_metadata_pil(image, meta)
out = Path(args.output).expanduser().resolve()
out.write_bytes(png_bytes)
_out({"output": str(out), "fields": list(meta.keys())})
def cmd_inject_name(args):
names = [generate_injection_filename(args.template, args.channels) for _ in range(args.count)]
_out({"filenames": names})
def cmd_templates(_args):
templates = {}
for name in get_jailbreak_names():
content = get_jailbreak_template(name)
templates[name] = content[:120] + ("..." if len(content) > 120 else "")
_out({"templates": templates, "count": len(templates)})
def cmd_analysis_tool(args):
p = Path(args.image).expanduser().resolve()
if not p.exists():
_err(f"File not found: {p}")
result = execute_action(args.action, p.read_bytes())
if hasattr(result, "to_dict"):
_out(result.to_dict())
else:
_out({"result": str(result)})
def cmd_list_tools(_args):
tools = list_available_tools()
_out({"tools": tools, "count": len(tools)})
def cmd_crypto_status(_args):
s = crypto_status()
_out(s if isinstance(s, dict) else {"status": str(s)})
# ---------------------------------------------------------------------------
# Parser
# ---------------------------------------------------------------------------
def build_parser():
p = argparse.ArgumentParser(prog="stegg-cli", description="ST3GG steganography CLI for agents")
sub = p.add_subparsers(dest="command", required=True)
# encode
enc = sub.add_parser("encode", help="Encode data into an image")
enc.add_argument("-i", "--input", required=True, help="Carrier image")
enc.add_argument("-t", "--text", help="Text payload")
enc.add_argument("-f", "--file", help="File payload")
enc.add_argument("-o", "--output", help="Output path")
enc.add_argument("--channels", default="RGB")
enc.add_argument("--bits", type=int, default=1)
enc.add_argument("--strategy", default="interleaved")
enc.add_argument("--seed", type=int, default=0)
enc.add_argument("--password", default="")
enc.add_argument("--no-compress", action="store_true")
enc.set_defaults(func=cmd_encode)
# decode
dec = sub.add_parser("decode", help="Decode data from an image")
dec.add_argument("-i", "--input", required=True, help="Encoded image")
dec.add_argument("-o", "--output", help="Save extracted data to file")
dec.add_argument("--no-auto", action="store_true", help="Disable auto-detect")
dec.add_argument("--channels", default="RGB")
dec.add_argument("--bits", type=int, default=1)
dec.add_argument("--strategy", default="interleaved")
dec.add_argument("--seed", type=int, default=0)
dec.add_argument("--password", default="")
dec.set_defaults(func=cmd_decode)
# analyze
ana = sub.add_parser("analyze", help="Analyze image for steg indicators")
ana.add_argument("image", help="Image to analyze")
ana.add_argument("--full", action="store_true", help="Full 264-function analysis")
ana.set_defaults(func=cmd_analyze)
# detect
det = sub.add_parser("detect", help="Quick STEG v3 header check")
det.add_argument("image")
det.set_defaults(func=cmd_detect)
# capacity
cap = sub.add_parser("capacity", help="Calculate carrier capacity")
cap.add_argument("image")
cap.add_argument("--channels", default="RGB")
cap.add_argument("--bits", type=int, default=1)
cap.set_defaults(func=cmd_capacity)
# inject-chunk
ic = sub.add_parser("inject-chunk", help="Inject PNG text chunk")
ic.add_argument("-i", "--input", required=True)
ic.add_argument("-o", "--output", required=True)
ic.add_argument("--type", default="tEXt")
ic.add_argument("--keyword", default="Comment")
ic.add_argument("--text", required=True)
ic.add_argument("--compressed", action="store_true")
ic.set_defaults(func=cmd_inject_chunk)
# read-chunks
rc = sub.add_parser("read-chunks", help="Read PNG chunks")
rc.add_argument("image")
rc.set_defaults(func=cmd_read_chunks)
# inject-exif
ie = sub.add_parser("inject-exif", help="Inject EXIF metadata")
ie.add_argument("-i", "--input", required=True)
ie.add_argument("-o", "--output", required=True)
ie.add_argument("--comment", default="")
ie.add_argument("--author", default="")
ie.add_argument("--description", default="")
ie.add_argument("--title", default="")
ie.add_argument("--custom-fields", default="")
ie.set_defaults(func=cmd_inject_exif)
# inject-name
inj = sub.add_parser("inject-name", help="Generate injection filenames")
inj.add_argument("--template", default="universal_decoder")
inj.add_argument("--channels", default="RGB")
inj.add_argument("--count", type=int, default=1)
inj.set_defaults(func=cmd_inject_name)
# templates
tmpl = sub.add_parser("templates", help="List jailbreak templates")
tmpl.set_defaults(func=cmd_templates)
# analysis-tool
at = sub.add_parser("analysis-tool", help="Run a specific analysis function")
at.add_argument("image")
at.add_argument("action")
at.set_defaults(func=cmd_analysis_tool)
# list-tools
lt = sub.add_parser("list-tools", help="List all analysis actions")
lt.set_defaults(func=cmd_list_tools)
# crypto-status
cs = sub.add_parser("crypto-status", help="Check encryption availability")
cs.set_defaults(func=cmd_crypto_status)
return p
def main():
parser = build_parser()
args = parser.parse_args()
try:
args.func(args)
except SystemExit:
raise
except Exception as e:
_err(str(e))
if __name__ == "__main__":
main()