This release is a pre-release and may not be stable for production use.
claude-workflow-viz
Turn any Claude Code workflow run into a live Mermaid flowchart — passively, from hooks.
Register one hook command, run claude-workflow-viz serve, and every workflow run in the
repo lights up a graph in your browser: stages running, passing, failing, and retrying as the
workflow's agents do the work. The visualiser observes the run from the outside; it never
touches the workflow itself. The step graph is configuration data, not tied to any one workflow.
flowchart LR
run["Claude Code workflow run"] -- "hook event (stdin)" --> hook["claude-workflow-viz hook"]
hook -- "POST /ingest (Bearer token)" --> serve["claude-workflow-viz serve"]
serve -- "SSE snapshot + events" --> ui["browser UI (vendored Mermaid)"]
- An observer, not a driver. Hooks forward events to a local server. If the visualiser dies mid-run, the workflow continues unaffected.
- Silent by design. The hook path always exits 0 and writes nothing to stdout or stderr — it can never disturb a session.
- Zero build step. One dependency (PyYAML), stdlib HTTP, and Mermaid vendored locally. No CDN, no bundler, no network at runtime.
Core features
| Feature | What it gives you |
|---|---|
| Live step graph | Nodes light up as your workflow's agents run: running steps pulse, finished steps show pass/fail, retries render on the node. |
| Verdict-driven colours | A finished step's status is derived from its journal verdict (APPROVED, CHANGES_REQUESTED, FAIL, …) via a shared JSON rules file — one home for the strings the UI matches. |
| Configurable graph | One .viz.yaml file maps your workflow's stages and step keys to named nodes with pass/fail routing. Without one, a linear graph is built from the stages as they first appear. |
| Session-scoped state | Concurrent sessions are tracked separately: per-session steps, status history, run summaries. Bounded (64 sessions LRU, 5000 steps) so a long-lived server stays small. |
| Locked-down transport | Random 32-byte token, loopback binding only, Host header allowlist, body-size caps, and a connection file written 0600 and removed on shutdown — while it still holds the server's own token. |
| Self-contained UI | Static files served from the package with Mermaid vendored in-tree. Everything works offline. |
The fixed policies are the product: the token is never logged, an invalid or missing token is a 401, an over-large payload is a 413, a malformed event is a 400 with a typed error code, and the hook side swallows everything — the visualiser gets events only when everything is well-formed.
Installation
Prerequisites
| Requirement | Detail |
|---|---|
| Python | ≥ 3.12 |
| Claude Code | Only for the hook side — the serve side runs anywhere. |
pipx install .
claude-workflow-viz --help
Usage
Subcommands
| Command | Purpose |
|---|---|
claude-workflow-viz serve [--port N] [--graph FILE] |
Run the local server (127.0.0.1 only) and the web UI. |
claude-workflow-viz hook |
Receive a Claude Code hook event on stdin and forward it to the server. |
claude-workflow-viz install [--viz-dir PATH] [--force] |
Wire a repo: verify, register the five hooks, create the /viz command. |
1. Start the visualiser.
claude-workflow-viz serve
# claude-workflow-viz listening on http://127.0.0.1:8322
# open the UI at http://127.0.0.1:8322/?token=<random-token>
serve prefers port 8322; when that port is busy it falls back to an OS-picked free port.
--port 0 forces an OS-picked port. The actual bound port is printed on start and written
to the connection file. Open the printed URL — the page reads the token from the query
string and uses it for /graph and /stream. Ctrl-C shuts the server down and removes
the connection file.
hook finds serve through the connection file, so the two sides never share a config file:
| State directory, first match wins | |
|---|---|
1. $CLAUDE_WORKFLOW_VIZ_STATE_DIR/server.json |
used as is |
2. $XDG_STATE_HOME/claude-workflow-viz/server.json |
standard XDG state |
3. ~/.local/state/claude-workflow-viz/server.json |
default |
The file has mode 0600 and the directory (when created by the tool) mode 0700. It is created
with that mode from the start in a temporary file next to the target and moved into place with
os.replace, so a reader never sees a partial file and the token is never briefly world
readable. serve removes it on shutdown only while it still holds this server's token; a file
written by a newer server is left in place. A missing, unreadable or malformed file means no
server is running, and hook quietly does nothing.
2. Register the hooks in the repo where the workflow runs
(.claude/settings.local.json). The install subcommand writes these for you
(see "Installer" below); this JSON is the contract it writes:
{
"hooks": {
"PreToolUse": [
{"matcher": "Workflow", "hooks": [{"type": "command", "command": "claude-workflow-viz hook", "async": true}]}
],
"PostToolUse": [
{"matcher": "Workflow", "hooks": [{"type": "command", "command": "claude-workflow-viz hook", "async": true}]}
],
"SubagentStart": [
{"hooks": [{"type": "command", "command": "claude-workflow-viz hook", "async": true}]}
],
"SubagentStop": [
{"hooks": [{"type": "command", "command": "claude-workflow-viz hook", "async": true}]}
]
}
}
async keeps the hook off the workflow's critical path; claude-workflow-viz hook reads the
payload from stdin, forwards it, and exits 0 no matter what.
3. Run the workflow. The graph updates live.
What a run looks like:
- serve binds 127.0.0.1, generates the token, writes the 0600 connection file, and prints the UI URL.
- The workflow runs. Each
SubagentStart/SubagentStopand WorkflowPreToolUse/PostToolUsebecomes an event and lands in the server's run state, keyed by session. - Nodes light up. Steps are matched to nodes by
stageor exactstep_key; a node without a step renders pending. - Verdicts paint the graph. When a step finishes, its journal verdict
(
APPROVED,PASS, …) decides the node's pass/fail colour; unknown verdicts fall back to the step's reported status. - Routes fire. Pass and fail edges highlight, retries re-open nodes, and
exhausting
max_roundsroutes to theon_rounds_exhaustedterminal. - Skipped stages grey out. A non-terminal stage the run bypassed renders skipped — grey dashed stroke and muted fill — once a later stage arrives or the run ends.
Installer
claude-workflow-viz install wires a repo in one command:
claude-workflow-viz install [--viz-dir <path>] [--force]
It first verifies the tool by running claude-workflow-viz hook once with empty stdin.
Then it writes the five hook registrations into .claude/settings.local.json.
The registrations are the four hooks shown above plus SessionEnd.
It also creates .claude/commands/viz.md, the /viz command the agent fallback runs.
Existing unrelated hooks are preserved, and an already-wired repo is left byte-for-byte unchanged.
Re-running is safe: the second install re-verifies and reprints the summary but writes nothing.
--viz-dir <path>picks the directory for agent-generated.viz.yamlfiles. The default is.claude-local/viz/. The choice is recorded in the/vizcommand file.--forceoverwrites an existing.claude/commands/viz.md; without it the command is skipped.
On the next workflow run the PreToolUse hook bootstraps the visualiser, as described in
"Invoke-time bootstrap".
Invoke-time bootstrap
When a workflow starts, the Workflow PreToolUse hook bootstraps the visualiser.
No manual serve step is needed:
- The hook reads the connection file and probes
GET /graphto see whether a server is already running. A live server is reused unchanged. - The hook statically parses the invoked workflow's JavaScript
(
<repo>/.claude/workflows/<name>.js) into the graph contract and writes<state>/graphs/<name>.viz.yaml. Other workflows' caches are left untouched. - If no server is live, the hook spawns
servedetached on the preferred port 8322, falling back to a free port when busy. The server's output goes to the state-dir debug log, never the hook's stream. - The hook waits at most 10 seconds for the connection file, then opens the dashboard at
/?token=<token>&workflow=<name>in the default browser.
If the workflow cannot be parsed, no cache is written and the server starts in linear mode;
the dashboard shows the unmapped-step warning, and the state-dir debug log carries the
parser's failure summary. SessionEnd stops the server by posting /shutdown.
Server endpoints
| Endpoint | Auth | Behaviour |
|---|---|---|
POST /ingest |
Authorization: Bearer <token> |
One JSON event, at most 64 KiB. 204 applied, 400 bad JSON or invalid event, 401 missing or wrong token, 413 body too large. |
POST /shutdown |
Authorization: Bearer <token> |
Stops the server and removes its connection file (the same sequence as Ctrl-C). 401 on a missing or wrong token, 204 on success. |
GET / |
none (the page itself holds no run data) | The web UI HTML. Open it as /?token=<token>; the page reads the token from the URL and uses it for /graph and /stream. |
GET /static/<file> |
none | A file from the package's static directory (app.js, viz-core.js, styles.css, mermaid.min.js, status_rules.json, ...). Only existing .html, .js, .css and .json files are served; anything else, including VENDORED.md and mermaid.LICENSE, is 404. |
GET /graph?token=<token>[&workflow=<name>] |
token query parameter |
JSON {"graph", "source", "status_rules"}. source is config when a graph was supplied (serve --graph or the per-workflow registry), else linear. workflow=<name> serves that workflow's graph from <state>/graphs/<name>.viz.yaml; an unknown name falls back to linear. 401 on a missing or wrong token. |
GET /stream?token=<token> |
token query parameter (a browser EventSource cannot set headers) |
text/event-stream. First a snapshot event with the current run state, then one event per applied event, with a : keep-alive comment every 15 seconds. 401 on a missing or wrong token. |
Every endpoint accepts only a Host header of 127.0.0.1:<port> or localhost:<port> (the
IPv6 form [::1]:<port> is not accepted because the server binds IPv4 only); a missing or any
other Host gets 421 before the token check. A request body that stalls for 10 seconds, such
as fewer bytes than its Content-Length, closes the connection. Any other path returns 404.
Run state and limits
The snapshot event (and RunState.snapshot()) has this shape per session:
{"sessions": {"<session_id>": {
"steps": {"<step_key>": {"statuses": ["running", "pass"], "summary": "", "finished": true,
"agent_id": "", "run_id": "", "slice": "", "stage": "",
"round": null, "phase": "", "result": null,
"node_status": "pending | running | pass | fail | retry"}},
"runs": {"<run_id>": {"status": "running | finished", "step_keys": ["<step_key>"]}},
"events": []}}}
Steps with an agent_id are paired by it, so a start and stop pair even when their step keys
differ; a stop with no start creates a finished step, and a start with an agent_id arriving
after its stop does not reopen it. A start without an agent_id on a finished step is a retry
round: it appends its status and reopens the step. A run is running while any step is
unstopped and finished once it has steps and all have stopped; a step start that opens a step
returns a finished run to running. An explicit run_stop is final for that run id: a later
run_start or step start never returns it to running (step fields still update). The snapshot
flags each such run with stopped: true, so a client that reconnects after the run_stop event
has aged out of the event log keeps the run finished.
Run state is bounded so a misbehaving client cannot grow server memory without limit:
| Limit | Value | When exceeded |
|---|---|---|
| Sessions | 64 | A new session evicts the least recently updated one (every applied event updates its session; this is not the oldest created). |
| Steps per session | 5000 | An event that resolves to a new step key creates no step and does not change run status. It is still logged in events and delivered to /stream subscribers. Existing steps keep updating. |
| Statuses per step | 100 | Only the last 100 are kept, in order; the oldest is dropped. |
A step's result is copied when stored, so later changes to the incoming event do not affect it.
Graph configuration
serve --graph FILE loads the step graph from a .viz.yaml file; an invalid file makes serve
exit with an error instead of starting. Without --graph the server builds a linear graph: one
node per distinct stage seen in events (first-seen order), ending in done. A stage named
done, blocked or needs-context gets the node id stage-<stage> (plus a numeric suffix if
that id is taken) so it never collides with a terminal node; the node keeps the original stage
and label.
Schema (see examples/implement.viz.yaml):
| Key | Meaning |
|---|---|
name, entry |
Graph name and the id of the first node. |
max_rounds, on_rounds_exhausted |
Optional retry limit and the node to go to when it is used up. |
step_pattern |
Optional regex with named groups slice, stage, round; stage is required. Rejected: longer than 200 characters; nested quantifiers such as (a+)+, (a*)*, (?:a+)*, (a{1,})+; Python-only syntax the UI cannot convert ((?P=name), inline flags (?i) or (?i:...)); anything Python cannot compile, including variable-width lookbehind. (?P<name>...) is allowed. |
nodes[] |
id, label, one of stage or step_key to match events, terminal: true for end nodes, on_pass / on_fail as the ids to go to next. |
A value may not be one node's stage and another node's step_key; serve --graph rejects such
a file and names both nodes and the value. One node may use the same value for both. A duplicate
key anywhere in the file is rejected with the key and its line number.
name: implement
entry: pre-flight
max_rounds: 3
on_rounds_exhausted: blocked
nodes:
- {id: pre-flight, label: Pre-flight, step_key: pre-flight/head-check, on_pass: planner, on_fail: blocked}
- {id: develop, label: Develop, stage: develop, on_pass: gate, on_fail: blocked}
- {id: gate, label: Gate, stage: gate, on_pass: review, on_fail: develop}
- {id: done, label: Done, terminal: true}
- {id: blocked, label: Blocked, terminal: true}
Every validation failure returns a typed error code (GraphError) naming the node and field —
never a stack trace.
Static parse subset
At invoke time the visualiser statically parses the invoked workflow's JavaScript into the same .viz.yaml schema above.
The parser code is the authoritative subset definition.
Anything outside the subset is a named failure — never a guessed graph — and degrades to a hand-authored fallback.
| Construct | Parsed as |
|---|---|
export const meta = { name } |
graph name |
const MAX_ROUNDS = <int> |
max_rounds; if (round >= MAX_ROUNDS) return X → on_rounds_exhausted |
agent(...) with a string-literal label |
a step_key node matched on the whole label |
agent(...) with a ${slice}/<stage>:r${round} template label |
a stage node matched on the literal stage |
while/for with continue back to an earlier agent |
a loop-back on_fail edge to the re-entered node |
early return DONE / BLOCKED / NEEDS_CONTEXT |
terminal nodes done / blocked / needs-context |
an if-guarded agent with no agent in the else |
a normal node (the guard is not encoded) |
Promise.all of agent(...) calls |
one node per member |
Each construct outside the subset produces one typed ParseFailure, collected exhaustively in one pass.
The codes are unsupported-label, missing-max-rounds, unsupported-control-flow, unsupported-terminal, unsupported-conditional, and unsupported-parallel, plus missing-meta and no-agent-calls.
A report, not a partial graph, is the result whenever any failure is found.
Development environment
uv run pytest
uv run ruff check # lint (E, F, W, I, B, UP, SIM, N)
uv run ruff format --check # formatting
node --test tests/js # the browser-side status logic in static/viz-core.js; needs node
- Development is strict TDD — the history is red/green slices; each slice's red commit names the test it fails.
tests/fixtures/probe_payloads.jsonlholds real Claude hook payloads captured from a live run; the adapter tests replay them.docs/plan.mdrecords the architecture decisions — the hook-driven design and why the transcript-tail adapter was deferred (the Phase 0 probe proved it unnecessary).
Agentic dev loop
The repo's default development mode is the user driving the agentic-dev-loop construction pipeline — see AGENTS.md ("Default development mode — ASK BEFORE CODING").
pipx install agentic-dev-loop # outside the workspace — never editable, never in the repo .venv
export OPENAI_API_KEY=... # no .env files — the engine refuses a run when the workspace has .env*
./trigger-agentic-dev-loop.sh --plan planning-docs/your-plan.md
- The trigger pipx-upgrades the engine on every run, so a stale install can't reject newer pipeline configs.
- The pipeline prints a thread id on every run;
resume a crashed run with
--resume <thread-id>. - The viz web server runs on
http://localhost:8321by default; disable with--no-viz. agentic-dev-loop/is an example to adapt:pipeline.yaml(stepsdev → restore → test → review → qa → commit,deps_policy: open) andsdk_agents/{dev,review,qa}.agent.md. The example restore/test commands install the project and the dev tools directly (pytest,ruff,pip-audit) — dev deps live in uv's[dependency-groups], so there is no.[dev]extra to install.
Agent instructions and standards
AGENTS.md— ask-before-coding default, TDD, mock policy, writing/tone rules.coding-standards-ddd.yaml— DDD + simple-code standards;/load-backend-rulesloads it into a session before backend work..aidlc/aidlc-rules/— AWS AI-DLC ruleset; the entry point isaws-aidlc-rules/core-workflow.md. Working docs (plans, user stories, unit-of-work, state) live inaidlc-docs/..opencode/— Builder (default), Planner, and Readonly agents; commands/apply-aidlc-patches,/epic-status,/export-opencode-tools-to-claude,/figma,/load-backend-rules,/postmortem,/software-selection,/ticket-details,/unit-status,/update-subtask; skillsbrainstorming,hypothesis-generation,writing-plans.
Contributing
- Keep the two contracts sacred: the hook is always silent (exit 0, empty stdout/stderr,
swallows
BaseException), and the snapshot JSON shape is the UI's API — it must not change without the JS tests. - All validation returns typed result objects (
EventError,GraphError) — no exceptions for expected conditions. - Mermaid is vendored (
src/claude_workflow_viz/static/) — no CDN.
Built on Claude Code hooks and Mermaid (vendored, MIT).
Metadata
Release files for claude-workflow-viz 1.0.0a1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| claude_workflow_viz-1.0.0a1.tar.gz | 1.0 MB | With provenance | Details for claude_workflow_viz-1.0.0a1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| claude_workflow_viz-1.0.0a1-py3-none-any.whl | Python 3 | none | any | Details for claude_workflow_viz-1.0.0a1-py3-none-any.whl |
Total release size: 2.1 MB
Release files / claude_workflow_viz-1.0.0a1.tar.gz
| Download URL | claude_workflow_viz-1.0.0a1.tar.gz |
|---|---|
| Size | 1.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8e303d0bb00fea3d0e720cad4434d78dfd6f7cd115d5f75bba14c385fcf94b60
|
|
BLAKE2b-256 checksum How to use checksums |
3cba216a41bc79b3002c795a85486bd081f2cab0cb272e313261d1d617af0523
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / claude_workflow_viz-1.0.0a1-py3-none-any.whl
| Download URL | claude_workflow_viz-1.0.0a1-py3-none-any.whl |
|---|---|
| Size | 1.1 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b110ca3fb7c1c6d125288eff0c787d405b083ec7878a8d3b6d1250992a7e7698
|
|
BLAKE2b-256 checksum How to use checksums |
8b85019ec21df6b9a433fafdc84e752355f5c431e69c2fc1a5917d9b3342082b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for claude-workflow-viz 1.0.0a1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for claude_workflow_viz-1.0.0a1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.