Blaze Hammer is an asynchronous API testing and traffic-generation tool built with Python. It generates dynamic payloads and headers through placeholder injection (Faker + built-ins), sends configurable asynchronous load over HTTP/1.1 and HTTP/2, and reports production-grade statistics through a live Rich dashboard or plain CI-friendly output.
Blaze Hammer is intended for authorized API testing: your own services, staging environments, load-test endpoints, or systems you have explicit permission to test. It deliberately contains no features for bypassing authentication, rate limits, CAPTCHAs, WAFs, or other protections.
Blaze Hammer v1.0.0
Target: https://staging.example.com/api/register
--- Final Report ---
Duration: 12.42s
Requests/sec: 161.03
Completed: 2000/2000
Successful: 1987
Failed: 13
Latency ms P50/P90/P95/P99: 41/96/128/210
Status 200: 1987
Requires Python 3.13+.
pip install blaze-hammer # from PyPI, once published
# or from a checkout:
uv sync # creates .venv and installs blaze-hammer (editable)Verify:
blaze-hammer --version # blaze-hammer 1.3.0
blaze-hammer --helpAll of these entry points are equivalent — bh is a real console script,
not a shell alias:
bh ...
blaze-hammer ...
python -m blaze_hammer ...
python main.py <URL> ... # legacy compatibility shimThe recommended workflow is a project directory driven by blazehammer.yaml:
# Create a project (interactive wizard; --non-interactive for CI)
bh init
# Enter the project
cd my-api-test
# Check everything before sending traffic
bh validate
bh inspect
# Run using blazehammer.yaml (no arguments needed)
bh run # or simply: bh
# Override the target only - YAML values still apply
bh https://api.example.com/users
# Temporary overrides - the YAML file is never modified
bh https://api.example.com/users -n 1000 -c 50
bh run --rate 200 --timeout 30 --faker-locale bn_BDinit creates:
my-api-test/
├── blazehammer.yaml # project configuration (commented, minimal)
├── payload.json # example payload with placeholders
├── headers.json # example headers with placeholders
├── profiles/ # reusable per-endpoint configs
├── .gitignore # .env, results/, __pycache__/
└── .env.example # documented environment overrides
target: "https://example.com/api"
method: POST
requests: 100
concurrency: 10
delay: 0
payload: payload.json # relative to this YAML file
headers: headers.json
post_type: json
timeout: 10
retries: 0
faker:
locale: en_US
# seed: 12345 # reproducible generated data
output:
simple: false
# file: results.json # export summary (.json/.csv)Unknown keys are rejected with suggestions (concurency → Did you mean:
concurrency?), and payload:/headers: paths always resolve relative to
the YAML file itself — so cd /elsewhere && bh run --config ./proj/blazehammer.yaml
works exactly like running inside the project.
CLI arguments > environment variables > profile > blazehammer.yaml > defaults
Environment variables accept both prefixes; the plain form wins if both are set:
| Variable | Maps to |
|---|---|
BLAZE_HAMMER_TARGET / BLAZE_TARGET |
target URL |
BLAZE_HAMMER_METHOD / BLAZE_METHOD |
GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, TRACE |
BLAZE_HAMMER_REQUESTS, _CONCURRENCY, _DELAY, _RATE, _TIMEOUT, _RETRIES, _SEED |
numeric fields |
BLAZE_HAMMER_FAKER_LOCALE |
faker locale |
BLAZE_HAMMER_PAYLOAD_FILE, _HEADERS_FILE |
template files |
bh # target from blazehammer.yaml
bh <url> # positional URL overrides only the target
bh <url> -n 5000 # ...plus any other CLI overrides
bh run [URL] [opts] # explicit form - identical pipelinePassing both a positional URL and --url is rejected unless they match.
bh profile create register # writes profiles/register.json (override-only template)
bh profile list
bh profile show register
bh run --profile register # profile layers over blazehammer.yamlProfiles contain only what differs from the base project. The legacy
profiles list|show spelling keeps working.
# Dry run: resolve placeholders, build the request, send nothing
blaze-hammer run https://staging.example.com/api/register \
-m POST -p payload.json --dry-run
# Preview three generated requests, still send nothing
blaze-hammer run ... --preview-count 3
# Send load: 500 requests, 25 concurrent, ~50 req/s
blaze-hammer run https://staging.example.com/api/register \
-m POST -p payload.json -n 500 -c 25 --rate 50Running without any project or URL fails fast with an actionable message:
No Blaze Hammer project found
create a project with 'blaze-hammer init', pass a URL ('bh https://host/api'), or use --config
A typical payload.json:
{
"username": "{faker.user_name}",
"email": "{email(prefix=user_, length=10)}",
"password": "{password(length=16, symbols=true)}",
"plan": "{choice(free, pro)}",
"trace_id": "{uuid}"
}Every request resolves placeholders freshly; with --seed the whole sequence is reproducible.
| Command | Purpose |
|---|---|
run [URL] |
Execute a load test (also hosts --dry-run, --preview, --json-diff) |
inspect [URL] |
Validate configuration and explain it, with one generated request |
validate [URL] |
Validate configuration, files and placeholders only |
placeholders [QUERY] |
List placeholder syntax; placeholders faker [sub] browses Faker |
profiles list / profiles show NAME |
Manage reusable test profiles |
interactive |
Guided configuration flow, then run |
version |
Print version (--version also works) |
completion SHELL |
Print completion script: bash, zsh, fish, powershell |
Target & body:
| Option | Description |
|---|---|
-m, --method GET|POST |
HTTP method |
-p, --payload FILE |
JSON payload template (POST body) |
--headers FILE |
JSON headers template (all methods) |
-dh, --disable-headers |
Ignore the headers file |
-pt, --post-type json|form |
JSON or form-urlencoded body |
-fp, --file-payload |
Multipart attachments from blaze_hammer/ext/attachments.py |
Load shaping:
| Option | Description |
|---|---|
-n, --requests N |
Total requests (default 100) |
-c, --concurrency C |
Max in-flight requests (default 100) |
-d, --delay SECONDS |
Pause between request launches |
--rate RPS |
Target throughput via token bucket |
-t, --timeout SECONDS |
Per-request timeout (default 30) |
--retries N |
Retry connection failures/timeouts/5xx-class statuses |
--seed INT |
Deterministic placeholder generation |
Output & diagnostics:
| Option | Description |
|---|---|
-pp / -pr / -ph |
Print each payload / response / headers |
--status 500,404 |
Only print those status codes |
--success-only / --failed-only |
Print only successes / failures |
--max-response-size CHARS |
Truncate printed/saved bodies (default 2000) |
--save-responses DIR |
Write summary.json, responses.jsonl, errors.jsonl |
--output FILE |
Export summary as .json or .csv |
-s, --simple |
Plain output for CI/pipes (auto-forced when stdout isn't a TTY) |
--log-level LEVEL / --log-file FILE |
Diagnostics to stderr/file (never stdout) |
--debug |
Full tracebacks on errors |
--yes |
Skip the large-run confirmation prompt |
Safety valve: runs of ≥ 10,000 requests ask for confirmation on a TTY. Use --yes
to skip it in scripts.
Everything below keeps working unchanged:
python main.py https://host/api -n 200 -c 10 -m POST -pp
python main.py --json-diff payload_example.json # offline diff mode
blaze-hammer https://host/api -n 200 # bare URL routes to `run`Exit codes: 0 completed · 1 configuration/validation failure · 2 usage error ·
130 interrupted (Ctrl+C). --json-diff intentionally keeps its historical exit code 1.
Placeholders are resolved per request, inside both payload and header templates. The authoritative, always-in-sync reference is the registry itself:
blaze-hammer placeholders # full table: syntax, return type, description
blaze-hammer placeholders otp # detail card: arguments, defaults, exampleA placeholder occupying an entire value keeps its native JSON type; embedded placeholders become text (JSON-style):
{
"id": "{int(min=1,max=100)}", → "id": 42,
"active": "{bool}", → "active": true,
"value": "{null}", → "value": null,
"tags": "{list(item=int(min=1,max=9),length=3)}",
→ "tags": [4, 1, 9],
"message": "user-{int(min=1,max=9)}"
→ "message": "user-7"
}Legacy note: before 1.1.0 even lone {int(...)} produced a string. Lone numeric
placeholders now emit real numbers — update assertions in existing test suites
that relied on string values.
| Syntax | Returns | Description |
|---|---|---|
{uuid} |
string | UUIDv4 (seed-aware) |
{bool} |
boolean | Random true/false |
{null} |
null | Real JSON null |
{choice(a, b, c)} |
string | Random option |
{pick_line(file=words.txt)} |
string | Random line from a text file (cached per run) |
| Syntax | Returns | Description |
|---|---|---|
{str(length=16)} / {string(...)} |
string | Alphanumeric characters (default length 8) |
{hex(length=16)} |
string | Lowercase hexadecimal |
{digits(length=6)} |
string | Digits only — leading zeros preserved (048392) |
{letters(length=10, case=mixed)} |
string | Letters; case: mixed/lower/upper |
{alphanumeric(length=12)} |
string | Letters + digits |
{password(length=12, digits=true, uppercase=true, lowercase=false, symbols=false)} |
string | Password from enabled character sets |
| Syntax | Returns | Description |
|---|---|---|
{slug(length=12)} |
string | URL-safe lowercase slug — letters, digits, hyphens |
{username(length=10)} |
string | user_-prefixed username |
{token(length=32)} |
string | Auth-style token. Without --seed it uses cryptographic randomness; with --seed it becomes deterministic (reproducibility over strength — a deliberate, documented trade-off for test data) |
{otp(length=6)} |
string | Numeric OTP; leading zeros preserved |
| Syntax | Returns | Description |
|---|---|---|
{ipv4} / {ip} |
string | Valid IPv4 address |
{ipv6} |
string | Valid IPv6 address |
{port(min=1024, max=65535)} |
integer | Port number (defaults to the valid 1–65535 range) |
{user_agent} |
string | Realistic User-Agent via Faker |
Offsets use s, m, h, d, w units and apply everywhere shown below
({date(offset=-7d)}, {datetime(offset=+30m)}, {timestamp(offset=-1h)},
{unix(offset=-1h)}). Malformed offsets fail validation.
| Syntax | Returns | Description |
|---|---|---|
{date(offset=-7d, format=%Y-%m-%d)} |
string | strftime date (default today) |
{datetime(offset=+7d, format=%Y-%m-%dT%H:%M:%S)} |
string | Full datetime |
{timestamp(offset=-1h)} |
string | UNIX timestamp (legacy string type preserved) |
{unix(offset=-1h)} |
integer | UNIX timestamp as a native integer |
| Syntax | Returns | Description |
|---|---|---|
{int(min=1, max=100)} |
integer | Random integer in range |
{float(min=0, max=1, precision=2)} |
number | Rounded float |
{percent(precision=2)} |
integer/number | 0–100; integer unless precision given |
{price(min=10, max=500, precision=2)} |
number | Non-negative price-like float |
{negative} / {positive} |
integer | ±1..1,000,000 |
{zero} |
integer | Literal 0 |
{large_int(digits=20)} |
integer | Huge integer for boundary tests (≤1000 digits) |
Built for validation/limit testing:
| Syntax | Returns | Description |
|---|---|---|
{empty_string} |
string | "" |
{whitespace(length=5)} |
string | Exactly N spaces (0 allowed) |
{unicode(length=10)} |
string | Multi-script sample data; never lone surrogates, so JSON encoding is safe |
{special_chars(length=10)} |
string | Punctuation/symbols — no control characters that would corrupt terminals or JSON |
{long_string(length=1000)} |
string | Exactly N filler chars (hard cap 1,000,000) |
| Syntax | Returns | Description |
|---|---|---|
{list(item=int(min=1,max=100), length=5)} |
array | Native JSON array; item resolved fresh each element. Nested list() works up to depth 3. Item expressions must not contain braces — write item=faker.word, not item={faker.word}. Length is capped at 1000. |
To keep large runs safe, argument caps are enforced with clear errors:
strings ≤ 100,000 chars (long_string ≤ 1,000,000), lists ≤ 1,000 items,
large_int ≤ 1000 digits, tokens ≤ 1024 chars. Invalid arguments always fail
validation before any traffic — e.g. {otp(length=0)} reports
"Argument 'length' must be a positive integer."
Essentially the entire installed Faker API is available. The resolver dynamically discovers methods from the Faker instance — no hardcoded provider list.
{faker.name}
{faker.email}
{faker.city}
{faker.random_int(min=1,max=100)}
{faker.pybool}
{faker.date_of_birth(minimum_age=18, maximum_age=65)}
Both short and full provider paths work:
{faker.name}
{faker.providers.person.en_US.name}
{faker.providers.internet.en_US.email}
Keyword and positional arguments are supported. Values are safely parsed as Python literals (int, float, bool, None, str, list, tuple, dict):
{faker.pystr(min_chars=10, max_chars=20)}
{faker.random_int(min=1, max=100)}
{faker.date(pattern="%Y-%m-%d")}
{faker.pystr(10)}
When a faker token occupies an entire value, the native JSON type is preserved:
{
"id": "{faker.random_int(min=1,max=100)}",
"active": "{faker.pybool}",
"name": "{faker.name}"
}→
{
"id": 42,
"active": true,
"name": "John Smith"
}Embedded usage produces strings: "user-{faker.random_int(min=1,max=999)}" → "user-421".
Per-placeholder locale override:
{faker.name(locale=fr_FR)}
{faker.name(locale=bn_BD)}
Global default via CLI:
blaze-hammer run URL --faker-locale bn_BDThe global locale applies when no per-placeholder locale= is specified.
Custom providers in blaze_hammer/ext/providers.py are automatically available:
{faker.simple_example(category=greetings)}
{faker.advanced_example(category=greetings, language=bn, length=5)}
--seed applies to Faker instances. Identical seeds produce identical sequences:
blaze-hammer run URL --seed 12345blaze-hammer faker list # all available methods
blaze-hammer faker list email # filtered
blaze-hammer faker show random_int # signature, docs, sampleBrowse what's available: blaze-hammer placeholders faker city.
${VAR} references expand once per request generation and fail fast at validation
time if unset. Values that came from environment variables are treated as secrets
for display purposes:
{ "Authorization": "Bearer ${API_TOKEN}" }Before any traffic is sent, every token is checked against the registry, arguments are coerced exactly as they would be at runtime, Faker attributes/locales are probed, and referenced files must exist:
error: Invalid placeholder(s) found
[x] payload.json:3
{emal(prefix=x_)}
Unknown placeholder
Did you mean: email?
Unknown tokens are never silently sent as literal strings like [Invalid faker field: x]
(that was legacy behavior); they abort the run before request #1.
blaze-hammer run ... --seed 12345Guarantee: the same seed plus identical templates produces the identical request sequence — UUIDs, strings, choices, Faker values, picked lines, all of it. This works because only the scheduler task generates requests, strictly sequentially, regardless of network timing.
Not deterministic (by nature): {timestamp} and {date} reflect wall-clock time.
A profile is a JSON file holding any subset of run configuration:
// profiles/register.json
{
"target": "https://staging.example.com/api/register",
"method": "POST",
"payload_file": "payload.json",
"requests": 5000,
"concurrency": 40,
"rate": 100,
"retries": {"max_retries": 2}
}blaze-hammer run --profile register # profiles/register.json
blaze-hammer run --profile register -n 9000 # CLI wins
blaze-hammer inspect --profile register # validate & explainMerge priority, highest first:
CLI arguments → BLAZE_* environment variables → profile file → defaults
Supported env vars: BLAZE_REQUESTS, BLAZE_CONCURRENCY, BLAZE_DELAY, BLAZE_RATE,
BLAZE_TIMEOUT, BLAZE_RETRIES, BLAZE_SEED. Unknown profile keys are rejected
(typo protection).
Header/payload keys whose names look sensitive (authorization, cookie, api-key,
token, password, … — case-insensitive substring match) plus any key whose value came
from ${VAR} expansion are replaced with ***REDACTED*** in previews, -pp/-pr/-ph
output, saved responses, and logs. Add project-specific names via a profile's
sensitive_keys array.
Diagnostics (--log-level DEBUG, --log-file) go to stderr or a file, never into the
Rich output stream.
- concurrency caps simultaneous requests (bounded worker pool + bounded queue;
memory stays flat no matter how large
-nis). - rate paces request starts via a lazy token bucket (~R req/s, capacity 1 =
smooth sending). Combine freely:
--rate 100 -c 20means "at most 20 in flight, ~100 started per second". - delay adds an extra pause between launches; shutdown interrupts it instantly.
- retries are opt-in. Connection failures, DNS/TLS errors and timeouts retry with
exponential backoff (
base 0.5s, cap8s). Statuses502/503/504retry;429retries honoringRetry-After(capped at 30 s). Retries count separately in stats and respect rate pacing and Ctrl+C.
Timeout classification feeds the failure report: timeout, connection, dns,
tls, template, other.
Ctrl+C (or SIGTERM on POSIX) stops new work immediately, lets in-flight requests finish,
and prints partial statistics marked as interrupted. Exit code 130.
The engine collects Rich-free statistics: totals, retries, latency percentiles (P50/P90/P95/P99), status-code histogram, and categorized failure counts with samples.
blaze-hammer run URL --save-responses results/ # summary.json + responses.jsonl + errors.jsonl
blaze-hammer run URL --output results.json # machine-readable summary
blaze-hammer run URL --output results.csv # one-row CSV for spreadsheets/CIresponses.jsonl records redacted resolved payloads/headers and size-capped bodies
per request; failures land in errors.jsonl.
bh web starts a backend-only FastAPI + WebSocket server over the same
project and engine as the CLI. The Python package contains no web UI — the
dashboard is a separate React project that consumes this API.
bh web # http://127.0.0.1:8080 (defaults from blazehammer.yaml)
bh --web # identical shortcut
blaze-hammer web --port 9000Startup banner:
Blaze Hammer API v1.5.0
Project: ./blazehammer.yaml
Server: http://127.0.0.1:8080
API: http://127.0.0.1:8080/api/v1
WebSocket: ws://127.0.0.1:8080/api/v1/ws
Docs: http://127.0.0.1:8080/docs
Authentication: enabled
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/health |
public | liveness probe |
| GET | /api/v1/info |
public | name/version/features |
| POST | /api/v1/auth/login |
public | session cookie login |
| POST | /api/v1/auth/logout |
session | drop session |
| GET | /api/v1/auth/me |
session | current user |
| GET | /api/v1/config |
session | resolved run config (no secrets) |
| GET | /api/v1/config/templates |
session | editor text + SHA-256 revisions |
| POST | /api/v1/config/templates/save |
session+CSRF | atomic template save (422 invalid JSON / 409 conflict) |
| GET | /api/v1/placeholders/catalog |
session | autocomplete metadata: built-ins + dynamic Faker discovery |
| POST | /api/v1/config/save |
session+CSRF | PATCH-style YAML update (round-trip safe) |
| GET | /api/v1/profiles[/{name}] |
session | list/show profiles |
| POST | /api/v1/runs |
session+CSRF | start run → {run_id, status} |
| GET | /api/v1/runs[/{id}] |
session | history / single run |
| POST | /api/v1/runs/{id}/stop |
session+CSRF | cooperative stop |
| DELETE | /api/v1/runs |
session+CSRF | clear history |
| GET | /api/v1/runs/{id}/log |
session | response snapshots per request (bounded) |
| POST | /api/v1/preview |
session+CSRF | resolve sample requests |
| POST | /api/v1/validate |
session+CSRF | full pre-flight check |
Errors use one envelope: {"error": {"code": "NOT_AUTHENTICATED", "message": "…"}}. Mutating endpoints require the X-Requested-With: XMLHttpRequest header (CSRF guard).
Session-cookie authenticated; stable JSON events:
{"type": "run.started", "run_id": "abc123", "requested": 1000}
{"type": "stats.updated", "run_id": "abc123", "completed": 420, "success": 415,
"failed": 5, "rps": 82.4, "latency_ms": {"p50": 118, "p95": 240}}
{"type": "request.completed", "run_id": "abc123", "seq": 7, "index": 6,
"ok": true, "status": 200, "latency_ms": 124}
{"type": "run.completed", "run_id": "abc123", "status": "completed"}Multiple simultaneous clients are supported; state lives server-side, so a
page refresh recovers via GET /api/v1/runs.
POST /api/v1/config/save updates only the fields you send — it never
rewrites the whole file. Comments, key order, quoting, blank lines and
unknown/custom keys survive via round-trip YAML (ruamel). Send
config_revision (from GET /api/v1/config) for optimistic concurrency:
mismatches get 409 CONFIG_CONFLICT with current_revision. Explicit
null clears an optional value; omitted means unchanged. Empty patches
rewrite nothing. Successful changes broadcast config.changed with only
the changed field names.
Every request stores a bounded snapshot: status, monotonic-timed latency,
allowlisted response headers (content-type, content-length, server,
location, …), wire body_size, and a size-capped body excerpt with an
explicit truncation flag. Binary responses are never read or decoded
("[binary response omitted]"); undecodable text becomes
"[unable to decode response body]".
response_logging:
mode: errors # none | errors | all (default: errors)
max_body_bytes: 4096
max_headers: 20
# allow_headers: [content-type, server] # overrides the default allowlist
# redact_keys: [password, token] # JSON keys masked in excerptsCLI: --response-log all --response-body-limit 8192. Bodies are never read
at all under mode: none; caps apply in every mode. Successful saves of the
config PATCH preserve this section like any other.
Editors load text plus a payload_revision/headers_revision (SHA-256 of
the file contents). Saves send the revision they loaded:
POST /api/v1/config/templates/save
{ "payload": "{\n \"id\": \"{uuid}\"\n}", "payload_revision": "1ba045…" }{ "ok": true, "saved": ["payload"], "payload_revision": "b2c70a…", "headers_revision": null }- Only supplied fields are written; formatting is preserved verbatim (a trailing newline is added), placeholders untouched.
- Malformed JSON →
422 INVALID_JSONwith file/line/column. - File changed since load →
409 TEMPLATE_CONFLICTechoingcurrent_revision; never silently overwritten. - Writes are atomic (tmp → fsync → rename). On success all connected
WebSocket clients receive
{"type": "config.changed", "changed": ["payload"]}(names only, never contents).
GET /api/v1/placeholders/catalog returns built-in entries from the real
registry plus every Faker method discovered dynamically from the installed
Faker (incl. custom providers in blaze_hammer/ext/providers.py), grouped by
provider family with signature-derived parameters — cached per locale,
invalidated on configuration changes. The same frame is pushed as a
placeholder.catalog WebSocket event right after hello.
web:
enabled: true
host: "127.0.0.1"
port: 8080 # 0 = auto-assign
auth:
enabled: true
username: admin
password_hash: "scrypt$..." # or plaintext `password:` (hashed in memory)
cors:
enabled: true
origins: ["http://localhost:5173"]Precedence is the standard chain — CLI > env (BLAZE_HAMMER_WEB_*,
BLAZE_WEB_*, BH_WEB_*) > YAML > defaults. Authentication is enforced
server-side on every route and the socket; with auth enabled but no
credentials configured the server refuses to start.
# Terminal 1 — backend
cd blaze-hammer && bh web
# Terminal 2 — frontend
cd blaze-hammer-web && npm run dev # talks to http://127.0.0.1:8080blaze-hammer interactivePrompts for target, method, volume and templates, validates everything through the same pipeline, optionally shows a preview, then runs.
Resolve placeholders without sending anything and inspect what changed:
blaze-hammer run --json-diff payload.json # or: python main.py --json-diff payload.json
blaze-hammer validate -p payload.json # checks onlyNested structures are compared path-by-path ($.order.items[0].price), replacing the old
top-level-only table.
CLI (click) → Configuration (pydantic RunConfig) → Validation
→ Template resolution (registry + Faker bridge)
→ Request planning (single pipeline shared by preview/dry-run/run)
→ Execution engine (scheduler + bounded worker pool, rate bucket, retries)
→ HTTP client (one shared httpx.AsyncClient, HTTP/2)
→ Statistics collector (Rich-free)
→ Output (live dashboard / simple printer, exporters)
Design rules worth knowing before hacking:
- One request pipeline: preview, dry-run, and real execution all call
RequestPlanner.next_plan(). - The core engine never imports Rich; the dashboard polls
StatsCollectorsnapshots. - The scheduler alone resolves templates — this is what makes
--seedexact. - User-editable extension points live in
blaze_hammer/ext/:parsers.py– per-status-code output formatting dictsproviders.py– drop-in Faker providers (auto-registered)attachments.py– static multipart attachments
- The old
utils/custom_*.pyimport paths remain as shims.
See CHANGELOG.md for migration notes from 0.x.
uv sync --dev # install with dev tools
uv run ruff check . # lint
uv run ruff format . # format
uv run mypy # type check
uv run pytest # unit + integration tests (local server only, no internet)
uv run pytest --cov=blaze_hammer
uv build # build wheel/sdistTests spin up throwaway local HTTP servers; nothing touches the network. CI runs the same gates via GitHub Actions.
- Only point Blaze Hammer at systems you own or are authorized to test.
- Large runs require explicit confirmation interactively; scripts should use
--yesconsciously. - There is no IP rotation, fingerprint evasion, CAPTCHA solving, or auth bypass — by design.
- Treat payload/header templates as code:
${ENV}expansion means the process environment can inject credentials; usesensitive_keysto keep them out of reports.