Skip to main content
Pre-release

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:

  1. serve binds 127.0.0.1, generates the token, writes the 0600 connection file, and prints the UI URL.
  2. The workflow runs. Each SubagentStart/SubagentStop and Workflow PreToolUse/PostToolUse becomes an event and lands in the server's run state, keyed by session.
  3. Nodes light up. Steps are matched to nodes by stage or exact step_key; a node without a step renders pending.
  4. 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.
  5. Routes fire. Pass and fail edges highlight, retries re-open nodes, and exhausting max_rounds routes to the on_rounds_exhausted terminal.
  6. 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.yaml files. The default is .claude-local/viz/. The choice is recorded in the /viz command file.
  • --force overwrites 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:

  1. The hook reads the connection file and probes GET /graph to see whether a server is already running. A live server is reused unchanged.
  2. 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.
  3. If no server is live, the hook spawns serve detached 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.
  4. 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.jsonl holds real Claude hook payloads captured from a live run; the adapter tests replay them.
  • docs/plan.md records 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:8321 by default; disable with --no-viz.
  • agentic-dev-loop/ is an example to adapt: pipeline.yaml (steps dev → restore → test → review → qa → commit, deps_policy: open) and sdk_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-rules loads it into a session before backend work.
  • .aidlc/aidlc-rules/ — AWS AI-DLC ruleset; the entry point is aws-aidlc-rules/core-workflow.md. Working docs (plans, user stories, unit-of-work, state) live in aidlc-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; skills brainstorming, 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)

Source distribution for claude-workflow-viz 1.0.0a1
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)

Table of built distributions (wheels) for claude-workflow-viz 1.0.0a1
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page