Install
$ agentstack add skill-kevin-burns-claude-skills-report-builder ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Report Builder
The job is almost always the same shape: data in → one HTML file out, rendered by a template, viewable by double-clicking with no server. Keep that shape and the work stays small and reproducible. The two failure modes to design against are (1) non-determinism (charts that fetch live, timestamps that drift) and (2) unsafe data interpolation (raw values dropped into HTML or ``). Both are solved below.
The pattern
data (CSV / DataFrame / JSON)
└─ your prep code (pandas, optional) → a plain JSON "context"
└─ Jinja2 template (.html.j2) → scripts/render.py
└─ one self-contained report.html
Separate prep (project-specific, may use pandas) from render (generic, reusable). scripts/render.py is the generic half — point it at a template and a JSON context file and it emits HTML. Don't rewrite it per project.
Run render.py by its absolute path — it lives in this skill's base directory (announced when the skill loads, usually ~/.claude/skills/report-builder); a relative report-builder/scripts/render.py won't resolve from another repo. It has PEP 723 inline deps (jinja2), so it must run under uv — never bare python. Define an rbuild function per block (a function passes arguments correctly under bash and zsh, where a plain $VAR doesn't word-split; re-declare it per block since shell state doesn't persist between tool calls):
# uv is required here (jinja2); resolve it even when it's not on a bare PATH (non-interactive
# shells often drop ~/.local/bin or the Homebrew bin -> `uv: command not found`):
UV="$(command -v uv || ls "$HOME/.local/bin/uv" "$HOME/.cargo/bin/uv" /opt/homebrew/bin/uv /usr/local/bin/uv 2>/dev/null | head -1)"
rbuild() { "$UV" run "$HOME/.claude/skills/report-builder/scripts/render.py" "$@"; }
rbuild --template my_report.html.j2 \
--data context.json \
--out report.html --title "Q2 Cost Report"
Two rules that prevent the common bugs
- Autoescape stays ON.
render.pyenables it and usesStrictUndefined, so a typo'd
variable fails loudly instead of rendering blank, and text values can't inject markup. Only reach for | safe on content you generated and trust (e.g. a Markdown→HTML block you produced). Never | safe on data that originated from a user, a file, or an API.
- Pass data to JavaScript with
| tojson, never string-building. To hand a dataset to
Chart.js/Plotly, serialize it in the template: ```html
const chartData = {{ data | tojson }}; {# safe: escapes , quotes, unicode #}
``` This is correct and XSS-safe. Building JS by concatenating values is the classic injection hole — don't.
Chart.js or Plotly — pick per report
| Use Chart.js when | Use Plotly when | |---|---| | Standard charts: bar, line, pie, doughnut, radar | Interactive exploration: zoom, pan, rich hover, selection | | Small bundle, fast, dashboard-y look | Statistical/scientific: box, violin, heatmap, 3D, contour | | Few hundred → few thousand points | Large datasets, or users will drill into the data | | You want a clean default aesthetic with little config | You need subplots, dual axes, or precise scientific layout |
Default to Chart.js for status/metrics dashboards; reach for Plotly when the reader needs to interrogate the data, not just read it. Don't load both unless a report truly needs each.
Self-contained vs. CDN
- CDN (default for internal/online reports): Bootstrap 5, Chart.js, Plotly via
``. Smallest file, but needs network at view time.
- Vendored / inline (for portable, air-gapped, or archived reports): download the libs
and inline them, or commit them next to the report. Choose this when the report must open with no network, or must look identical years later. State which mode you chose in the report's provenance footer.
The starter template (assets/report-template.html.j2) uses CDN with the swap point marked. Pin a major version in the URL; verify the current version rather than guessing a patch number (see Fact-discipline).
Layout with Bootstrap 5
Lean on the grid and components instead of hand-rolling CSS:
- Page frame: a
container/container-fluid, a header row, thenrow/col-*for content. - KPIs:
cardcomponents in a responsiverow-cols-1 row-cols-md-3grid. - Tables: ``; for long tables, wrap in
table-responsive.
- Keep custom CSS to a small `` block for things Bootstrap doesn't cover (chart
container heights, print tweaks). Don't fight the framework.
Reproducibility (so the same data gives the same report)
- Pass any "generated at" timestamp in via the context (
--data), don't call
datetime.now() inside the template — that way a re-render of the same inputs is byte-stable, and tests can assert on output.
- Record provenance in a footer: source of the data, the render command, lib versions/mode
(CDN vs vendored). A report a reader can't trace is a report they can't trust.
Fact-discipline (library versions & APIs)
Don't assert a current Bootstrap/Chart.js/Plotly version or a chart-config field from memory — these drift. Confirm the version and the option name before pinning:
# resolve then fetch — see the c7search skill (avoid `ask`)
LIB=$(c7search resolve --library-name "chart.js" "bar chart options" --json --limit 1 | jq -r '.[0].id')
c7search docs "$LIB" --topic "bar chart options" --tokens 3000
Or check the official docs/CDN. If you can't verify, leave the version as a clearly-marked placeholder rather than inventing a number. (Defer to the fact-verifier agent when a version gates the build.)
Files in this skill
scripts/render.py— generic Jinja2 renderer (autoescape + StrictUndefined; PEP 723
inline deps; uv run-ready). Reuse it; don't reinvent it.
assets/report-template.html.j2— starter: Bootstrap 5 frame, KPI cards, a Chart.js
example wired through | tojson, a data table, and a provenance footer. Copy and adapt.
Quick start
- Copy
assets/report-template.html.j2into the project; adapt the markup. - Write a small prep step that produces a JSON context (`{"title": ..., "kpis": [...],
"chart": {...}, "rows": [...], "generated_at": "...", "provenance": {...}}`).
rbuild --template --data --out report.html(definerbuildas shown above).- Open
report.html. Verify chart renders, table populates, noUndefinederrors.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: kevin-burns
- Source: kevin-burns/claude-skills
- License: MIT
- Homepage: https://techblog.kevinburns.de
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.