Imported from pruiz/CodeCome (
.opencode/skills/exploit-recording/SKILL.md). Install upstream withnpx skills add pruiz/CodeCome --skill exploit-recording. Copyright stays with the author.
Exploit Recording Skill
Use this skill whenever a working proof-of-concept exploit must be turned into a reproducible demonstration recording.
This skill is target-agnostic and path-agnostic. The Phase 5 prompt pins the exact storage location inside the CodeCome workspace; this skill describes only the methodology.
Purpose
Recordings are first-class evidence. A recording must:
- be deterministic (driven by a script, not live typing),
- be replayable (asciinema cast or video file),
- be lightweight (KB to a few MB),
- be legible (font size, theme, contrast),
- show the full attacker workflow end-to-end.
Without a recording, an exploit narrative is harder to review and reproduce. Recordings make impact tangible to non-security reviewers.
Required outputs
For every successful TTY-driven exploit, produce:
exploit.cast # asciinema raw recording (primary, JSON, KB-sized)
exploit.gif # rendered preview (embeddable, legible)
exploit.mp4 # optional; only when GIF > ~3 MB or motion matters
reproduce.sh # idempotent reproducer script driving the recording
env.txt # sandbox environment metadata
README.md # how to play the cast and how to re-run reproduce.sh
For GUI/browser exploits that cannot be shown in a TTY, produce:
exploit.mp4 # ffmpeg capture (primary for GUI/browser demos)
exploit.gif # optional preview when small and legible
reproduce.sh # idempotent reproducer script driving the recording
env.txt # sandbox environment metadata
README.md # how to play the video and how to re-run reproduce.sh
env.txt minimum contents:
- kernel version,
- libc / runtime versions relevant to the target,
- target build commit or version,
- recording tool name and version,
- date of recording.
Recording path selection
Choose the recording path in this order:
- Decide whether the exploit can be demonstrated meaningfully in a terminal.
- Choose the execution context that makes the exploit easiest to run reproducibly.
- Use the recording path that matches that combination.
Use the TTY recording path when the exploit can be shown end-to-end in a terminal. Prefer:
asciinema+agg— preferred because.castis JSON, diff-able, deterministic on replay, and usually very small.asciinema+ containerizedagg(docker run --rm -v "$PWD:/data" ghcr.io/asciinema/agg)
Use the GUI/video recording path when the exploit fundamentally requires a browser, desktop UI, or other non-terminal interaction. Prefer:
ffmpegwithx11grabffmpegwithx11grabover a headlessXvfbdisplay`
Before downloading or installing tools, always check whether they are
already available in the chosen recording context. Only install tools if
the check fails. If you must download a tool, use the workspace-relative
tmp/ directory, never /tmp/.
Record the chosen context, toolchain, and versions in env.txt.
Recording absence is allowed only after the applicable recording path has been exhausted with evidence. Do not substitute a hand-written transcript for a real recording.
Absence of a recording does not block EXPLOITED status when a working
PoC exists, but the absence must be explicit, evidenced, and documented:
- per-attempt failure logs under
itemdb/evidence/<finding-id>/exploits/recordings/attempts/<tier>.log, - short summary under Limitations in
exploits/README.md, - explanation in the finding's
# Recordingsection, referencing the attempts directory.
Recording rules
- The target being attacked must be the sandboxed target.
- The exploit may be driven from inside the sandbox, from the host against the sandbox, or from a container / PTY wrapper, provided the PoC still uses the target's real external surface.
- Do not replace the real exploit path with in-process shortcuts, debugger control, or private source-tree access.
- No traffic, secrets, or destructive actions may leave the sandbox boundary.
- The exploit must be driven by
reproduce.sh, not typed live, so the cast is deterministic and auditable. reproduce.shmust be self-contained:- prepare any required state,
- run the exploit,
- print a clear
EXPLOIT SUCCESSFULmarker on success, - exit non-zero on failure.
- Target length 15–90 seconds. Hard cap 3 minutes. Cap idle pauses so
playback does not stall (
--idle-time-limit 2for asciinema). - No real credentials, customer data, or production hostnames in the
recording. Redact in
reproduce.shso the recording stays reproducible. - Each recording must visibly show, in order:
- the target version or commit,
- the command that triggers the vulnerability,
- the observable impact,
- a clear success marker.
- Verify the resulting cast plays cleanly
(
asciinema play exploit.cast) and that the GIF is below 3 MB. Re-render with adjusted parameters if not.
Decision flow
Follow this sequence:
- Decide whether the exploit is TTY-capable or GUI-only.
- Choose the recording context that makes the exploit easiest to run reproducibly: inside the sandbox, from the host against the sandbox, or inside a container / PTY-capable wrapper.
- If the exploit is TTY-capable, use the TTY recording path.
- If the exploit is GUI-only, use the video recording path.
- If the TTY recording path is chosen and direct
asciinema recdoes not have a PTY, walk the PTY-acquisition ladder. - If the selected recording path fails, capture failure evidence and document the absence explicitly. Do not replace a failed recording with a hand-written transcript.
Recording context selection
Before choosing specific commands, choose the execution context that best fits the exploit and the available tools. The recording may run:
- inside the sandbox,
- from the host against the sandbox,
- inside a container or PTY-capable wrapper.
Prefer the context that:
- can drive the exploit end-to-end,
- has the required tools available,
- keeps the workflow deterministic and replayable,
- adds the fewest extra moving parts.
Do not assume the host is always best, or that the sandbox is always best. Pick the context that makes the exploit easiest to reproduce faithfully, then apply the appropriate recording path there.
PTY-acquisition ladder
Use this ladder only for exploits that can be meaningfully demonstrated
in a terminal. If the exploit fundamentally requires a GUI or browser,
the PTY ladder is not applicable; skip to the ffmpeg path.
asciinema rec requires a pseudo-terminal. The shell environment that
agents run from (OpenCode, Claude Code, CI workers, container
non-interactive shells) frequently has no TTY attached, so a naive
asciinema rec --command … can fail its preflight even though all the
recording tools are installed. Do not stop at that first failure: there
are several reliable ways to give asciinema a PTY in non-interactive
contexts. Walk this ladder top-to-bottom and pick the first tier that
works in the recording context you selected. Record the chosen context
and tier in env.txt.
For every tier you attempt, run a short preflight first:
<tier-command-wrapping> asciinema rec \
--command "echo preflight" \
--overwrite tmp/preflight.cast 2> tmp/tier-error.log
asciinema play tmp/preflight.cast
If the preflight produces a non-empty cast that plays back cleanly, the
tier works — proceed to the real recording with that wrapping. If the
preflight fails, ensure you create the attempts directory first:
mkdir -p itemdb/evidence/<finding-id>/exploits/recordings/attempts/
and move tmp/tier-error.log to
itemdb/evidence/<finding-id>/exploits/recordings/attempts/<tier>.log,
then move to the next applicable tier. Do not silently skip applicable
tiers.
Tier 1 — direct (TTY already present)
asciinema rec \
--cols 100 --rows 30 \
--idle-time-limit 2 \
--command "bash reproduce.sh" \
--title "<finding-id> — <short title>" \
--overwrite exploit.cast
This works only when the chosen recording context already owns a TTY
(python3 -c 'import sys; print(sys.stdin.isatty())' prints
True). In most agent contexts it does not.
Tier 2 — script(1) PTY shim
script(1) (util-linux on Linux, BSD script on macOS) calls
forkpty() and gives the inner command a real PTY regardless of whether
the parent shell has one. This is usually the cheapest fix.
Linux (util-linux):
script -qfc 'asciinema rec \
--cols 100 --rows 30 \
--idle-time-limit 2 \
--command "bash reproduce.sh" \
--title "<finding-id> — <short title>" \
--overwrite exploit.cast' /dev/null
BSD / macOS:
script -q /dev/null asciinema rec \
--cols 100 --rows 30 \
--idle-time-limit 2 \
--command "bash reproduce.sh" \
--title "<finding-id> — <short title>" \
--overwrite exploit.cast
(Argument shape differs between the two script flavors. Detect which
one is on PATH before invoking it: util-linux supports script --version
and the -c/-f flags; BSD script does not.)
Tier 3 — unbuffer PTY shim
unbuffer ships with the expect package and also forks a PTY. Use it
when script(1) is unavailable or rejects the inner command:
unbuffer -p asciinema rec \
--cols 100 --rows 30 \
--idle-time-limit 2 \
--command "bash reproduce.sh" \
--overwrite exploit.cast
Tier 4 — Docker -t shim
When the host shell cannot supply a PTY but Docker is present, run
asciinema inside a container that allocates its own TTY via -t. This
is environment-independent and works even on CI runners.
Ephemeral container (no long-lived sandbox required):
docker run --rm -t \
-v "$PWD:/work" -w /work \
ghcr.io/asciinema/asciinema rec \
--cols 100 --rows 30 \
--idle-time-limit 2 \
--command "bash reproduce.sh" \
--overwrite /work/exploit.cast
When a long-lived sandbox container is already running and contains the
tools needed for reproduce.sh, prefer re-entering it with TTY
allocation rather than spawning a separate image:
docker exec -it <sandbox-container> bash -lc \
'asciinema rec \
--cols 100 --rows 30 --idle-time-limit 2 \
--command "bash /work/reproduce.sh" \
--overwrite /work/exploit.cast'
(docker compose exec also accepts -it; a long-lived stack is nice
but not required — fall back to docker run -t when no stack exists.)
Tier 5 — script(1) typescript + cast converter (last resort)
When tiers 1–4 all fail (rare, but possible in heavily restricted
environments), script(1) itself can capture a typescript with timing
data. The output is not natively an asciinema cast, but it can be
converted: asciinema cast v2 is JSONL with a one-line header object plus
[time, "o", text] records (see
https://docs.asciinema.org/manual/asciicast/v2/).
Capture with util-linux script:
script --timing=tmp/timing.log -qc 'bash reproduce.sh' tmp/typescript
Capture with BSD/macOS script:
script -t tmp/timing.log tmp/typescript bash reproduce.sh
The converter expects timing data in the usual delay count format. If
your local script(1) emits a different timing format, inspect the
generated timing file before conversion and document any adaptation in
recordings/README.md.
Then convert the timing/typescript pair to asciinema v2 JSONL with the repo helper:
python3 tools/script-to-asciinema.py \
tmp/timing.log tmp/typescript exploit.cast \
--width 100 --height 30 \
--command "bash reproduce.sh"
The helper uses only the Python standard library so it can run either
inside or outside the sandbox. Document the capture flavor and converter
command in recordings/README.md so the cast is reproducible.
Use this tier only when the PTY-bearing tiers above genuinely cannot run; a tier-5 cast lacks the fidelity of a real asciinema recording but is still better than no recording.
Tier 6 — ffmpeg / Xvfb
For GUI/browser exploits that cannot be expressed in a TTY at all, jump straight to the ffmpeg path documented further down. In that case the PTY ladder above is not applicable, and absence should be justified based on the GUI/video path that was attempted.
asciinema render (after a tier succeeds)
Render to GIF with legible defaults:
agg \
--font-family "JetBrains Mono,DejaVu Sans Mono,monospace" \
--font-size 20 \
--line-height 1.4 \
--theme monokai \
--speed 1.0 \
--fps-cap 12 \
exploit.cast exploit.gif
Optional MP4 (only if GIF exceeds ~3 MB or motion smoothness matters):
agg --font-size 20 --theme monokai exploit.cast tmp/exploit.gif
ffmpeg -y -i tmp/exploit.gif \
-movflags +faststart \
-pix_fmt yuv420p \
-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \
-c:v libx264 -preset slow -crf 28 \
exploit.mp4
The --cols 100 --rows 30 choice keeps .cast small while remaining
readable in agg at font-size 20. Do not exceed 120 columns; long output
should be paged or filtered inside reproduce.sh.
If agg is not on PATH but Docker is, use the containerized form:
docker run --rm -v "$PWD:/data" ghcr.io/asciinema/agg \
--font-size 20 --theme monokai \
/data/exploit.cast /data/exploit.gif
ffmpeg fallback (GUI / browser exploits)
Use this path only when the exploit cannot be expressed in a TTY (DOM XSS in a real browser, native GUI client, etc.).
Headless capture with Xvfb:
Xvfb :99 -screen 0 1280x720x24 &
XVFB_PID=$!
DISPLAY=:99 bash reproduce.sh &
ffmpeg -y \
-video_size 1280x720 \
-framerate 12 \
-f x11grab -i :99 \
-t 90 \
-c:v libx264 -preset slow -crf 30 \
-pix_fmt yuv420p \
-movflags +faststart \
exploit.mp4
kill "$XVFB_PID"
Legibility requirements for ffmpeg captures:
- Minimum capture resolution 1280x720.
- Force the target application or terminal to render at least 18 px
effective font size in the captured frame (configure browser zoom or
terminal font explicitly inside
reproduce.sh). - High-contrast theme; no translucent windows; no animated wallpapers.
- 12 fps is sufficient for exploit demos and keeps the file small. Do not exceed 24 fps.
- Target file size below 5 MB. If exceeded, increase
-crfor reduce duration.
README contents
The recording's README.md should include:
- a one-line description of what the recording demonstrates,
- the finding id,
- play instructions
(
asciinema play exploit.castand/or openexploit.gif/exploit.mp4), - how to re-run
reproduce.shagainst a freshly bootstrapped sandbox, - prerequisites (target running, attacker context, credentials provided via env vars, etc.),
- known limitations (probabilistic success rate, timing sensitivity, required tool versions).
Redaction guidance
Never let a recording capture:
- real production credentials,
- real customer data,
- real production hostnames or IPs,
- secrets pulled from outside the sandbox.
When the demonstration must show a credential or token (e.g. exfiltrated
from .env), inject a synthetic value in reproduce.sh before recording
so the captured material is reproducible and safe to publish.
Verification checklist
Before declaring the recording complete:
-
reproduce.shruns end-to-end and printsEXPLOIT SUCCESSFUL. -
asciinema play exploit.castrenders cleanly without long stalls. -
exploit.gifis below 3 MB and legible at displayed size. - If MP4 produced, it is below 5 MB and below 90 s.
- Target version/commit and success marker are visible in the recording.
- No real secrets or production identifiers are present.
-
env.txtdocuments the recording tool and target build. -
README.mddocuments how to play and re-run.