Local structured debugging for Python scripts shipped with Claude Code Agent Skills.
This repository currently contains these vertical slices:
- a dependency-free, vendorable Python debug client;
- a foreground Unix datagram collector;
- protocol validation, run-token authentication, and symlink-safe JSONL storage;
- a single-collector runtime lock and restart-safe receive sequencing;
- secure session and debug-run lifecycle management;
- an authenticated, same-UID Unix stream control transport and stdlib hook client;
- local JSON lifecycle CLI adapters for administration and tests;
- a Claude Code plugin with session, run, Skill-observation, Stop, and SessionEnd hooks;
- bounded collector startup/reuse, auditable stale-session leases, and five-minute quiescent idle shutdown;
- bounded, credential-free
runs.listand paginatedrun.getcontrol reads; - an imperative OpenTUI run/event/detail viewer with polling, stale-screen retention, search, and failed-run filtering;
- fail-open and end-to-end tests.
Retention, deterministic analysis, baseline comparison, subscriptions, and release packaging are intentionally not implemented yet.
- Python 3.12+
- uv for the development environment
- macOS or Linux with Unix domain datagram and stream sockets
- Bun 1.3+ for the TUI; development is validated with Bun 1.4.0
The checkout pins its development interpreter in .python-version. The Python code has no third-party runtime or test dependencies, so development commands use uv's dependency-free --no-project environment and do not require a lockfile.
uv run --no-project python -m unittest discover -s tests -v
uv run --no-project python -m compileall -q client src scripts testsuv run --no-project ./bin/skill-debug serveUse --help to override the persistent state root, ephemeral runtime root, registry, socket paths, the default 300-second quiescent idle exit, the default 600-second session lease, or the default 30-second per-run activity refresh interval. Pass --idle-exit-seconds 0 to keep a foreground collector running until signaled. The collector explicitly expires abandoned leases before health checks, revokes credentials first, and persists terminal run/session records for auditability.
Each successfully authenticated and persisted event refreshes its owning session and registry credential leases through a run-ID-only callback. Refresh persistence is throttled per run, so normal event volume does not rewrite and fsync lifecycle state for every datagram. Callback failures never undo event persistence or stop collection. A lifecycle-lock or state-I/O failure lasting longer than the session lease can still expire the run.
serve opens both the event datagram socket and <runtime-root>/control.sock. Hooks issue one NDJSON request through the control socket and never edit state files directly. A compact CLI adapter is available for inspection:
uv run --no-project ./bin/skill-debug request \
--op health \
--payload '{}'The control server accepts only the same effective UID and exposes the lifecycle plus bounded read operations documented in protocol/control-v1.md. It offers no command execution or arbitrary file access. runs.list returns recent credential-free summaries; run.get returns at most 500 events per page and all responses are limited to 1 MiB.
Install the locked TypeScript dependencies and start the terminal UI with Bun 1.3 or newer. The committed lockfile is not readable by Bun 1.2.23; upgrade Bun instead of deleting or regenerating bun.lock.
bun --version
# Homebrew installation:
# brew upgrade bun
# If Homebrew requires tap trust, inspect the formula and grant formula-only trust:
# brew tap-info oven-sh/bun
# brew cat oven-sh/bun/bun
# brew trust --formula oven-sh/bun/bun
# brew upgrade oven-sh/bun/bun
# Official Bun installation:
# bun upgrade
cd tui
bun install --frozen-lockfile
cd ..
./bin/skill-debug tuiThe first live-update slice polls the same-UID control socket every 500 ms; it does not read state files directly and does not open HTTP ports. A new selection starts at cursor zero, then incrementally catches up one page per request and retains a deduplicated rolling window of the newest 2,000 events. Disconnection leaves the last good screen visible with an explicit [STALE] marker; a cursor invalidated by external history replacement is shown as [LIVE PARTIAL] and restarts from zero. Use j/k or arrows to move, Tab to change panes, / to search events, f for failed runs, r to reload, ? for help, and q or Ctrl+C to quit. On terminals narrower than 100 columns, Detail is hidden until Enter toggles it.
The source-checkout launcher also routes collector commands, for example ./bin/skill-debug serve and ./bin/skill-debug health. The launcher is not installed by the Python wheel yet.
Direct lifecycle commands print JSON to stdout and remain available for administration and tests. Plugin hooks must use the control socket instead. The examples below use default state locations.
uv run --no-project ./bin/skill-debug session-register claude-session-id
uv run --no-project ./bin/skill-debug run-start \
--session-id claude-session-id \
--skill-name example-skill \
--skill-root /absolute/path/to/example-skill \
--task "validate fixture" \
--prompt-id prompt-id \
--endpoint unix-dgram:/absolute/path/to/collector.sock \
--idempotency-key prompt-id
uv run --no-project ./bin/skill-debug health
uv run --no-project ./bin/skill-debug run-finish \
--run-id 01J00000000000000000000000 \
--turn-outcome stopped \
--skill-outcome completed
uv run --no-project ./bin/skill-debug session-end claude-session-idSee protocol/control-v1.md for idempotency, state, error, and crash-ordering semantics.
Validate and load the plugin directly from this checkout:
claude plugin validate --strict .
uv run --no-project claude --plugin-dir .Then invoke:
/skill-debug:run <skill-name> -- <task>
# Explicit fallback when automatic lookup is unavailable or ambiguous:
/skill-debug:run <skill-name> --root <absolute-skill-root> -- <task>
When --root is omitted, the hook looks for exactly one matching SKILL.md under the current project’s .claude/skills/ directories from cwd through the repository root and under ~/.claude/skills/ (or $CLAUDE_CONFIG_DIR/skills/ when configured). Symlinked Skills resolve to their canonical target. No match, multiple different canonical matches, namespaced/plugin Skills, and locations outside those scopes are blocked with a request to provide --root; an explicit absolute root always takes precedence.
The SessionStart hook starts or reuses an identity-matching collector and registers the Claude session. UserPromptExpansion can recover from a transient SessionStart failure, refreshes the session lease, and registers a run. Skill tool hooks record only the named target, and Stop, StopFailure, and SessionEnd close lifecycle state. Turn-scoped hooks require Claude Code's nonempty official prompt_id; a missing ID is a silent no-op recorded only as bounded diagnostic metadata, never collapsed into a fabricated current-turn identifier. Hooks emit no stdout on successful or fail-open paths, so detailed events do not enter Claude's context. Invalid /skill-debug:run arguments and automatic root lookup that finds zero or multiple canonical matches are the intentional blocking outputs.
The complete interactive lifecycle is validated with Claude Code 2.1.251. In v0.1, invoke /skill-debug:run from an interactive Claude Code prompt: claude -p input is not user-typed command expansion and does not trigger the UserPromptExpansion registration hook.
Command arguments are parsed with POSIX shlex; the stored task is a whitespace-normalized reconstruction of tokens after --, not a byte-exact copy of the typed substring.
The plugin requires a Python 3.12+ executable named python3 or python. Test-only path overrides are SKILL_DEBUG_STATE_ROOT, SKILL_DEBUG_RUNTIME_ROOT, SKILL_DEBUG_REGISTRY, SKILL_DEBUG_IDLE_EXIT_SECONDS, and SKILL_DEBUG_SESSION_LEASE_SECONDS.
claude plugin validate --strict . validates plugin manifest/component structure, not runtime hook event compatibility or lifecycle behavior. The test suite therefore includes documented-shape event fixtures and real Unix-socket/subprocess coverage in addition to plugin validation.
The Python distribution is collector-only. The dependency-free client is deliberately not installed by the wheel because Agent Skills vendor it as a single source file. Copy client/skill_debug.py from this repository into an Agent Skill's scripts/ directory:
from skill_debug import SkillDebug
debug = SkillDebug.from_script(__file__)
with debug.script_run(argv=[]):
with debug.phase("validate"):
debug.log("input.validated", records=3)Without a matching active run or live collector, calls are no-ops and target behavior is preserved.
MIT