// changelog

Release
notes.

v0.4.38

2026-08-25GitHub ↗
MCP sessions survive instance restarts

Every Hugin instance shared one MCP socket path, and any instance shutting down deleted the file even when another was still serving it — after that, every agent connection silently fell back to spawning its own cold standalone copy: ~55s to first response under load, and one slow handshake marked MCP dead for that whole session. Shutdown now checks whether something is still listening before removing a socket, so agents attach straight to your running app and stay attached.

Google crawler plugin

A new google-crawler plugin brings the auth-gated Google crawler into the plugin surface: it proves a live session before crawling, refuses logged-out responses that would poison access-control testing, and mines batchexecute RPC ids, $rpc paths, Service/Method names, googleapis URLs and AIza keys from every page it walks.

Fuzzers push through 403 walls

Intruder and fuzzer runs no longer abort mid-campaign when a target starts answering 403 — a WAF wall or rate limit now ends the affected request path instead of killing the whole run, so the payloads you already queued still fire.

HTTP/3 fails fast and tells the truth

Hosts with broken QUIC are dropped after first failure instead of stalling every request behind them, draft-32 support is gone, and the h3 stats you read are the ones the client pool actually saw.

v0.4.37

2026-08-25GitHub ↗
BAC findings panel flags its own noise

The false-positive auto-verifier behind bac_audit results — mass-assignment probes that never echoed your injected field, SPA shells served byte-identical to every probed endpoint, public-by-design data where baseline and replay match exactly, pre-auth replays, and telemetry sinks that 2xx anything — now runs on the desktop findings panel too. MCP results and the panel classify through one shared implementation, so what a report says and what you see can no longer disagree. Flagged rows carry a Likely-FP chip naming the evidence class, a count sits next to the filters, and one toggle hides them from the view and every copy/export.

HTTP/3 egress stats are visible

Settings -> Alt-Svc gains an HTTP/3 runtime section with real numbers from the h3 client pool: connection counts, bytes sent and received, errors, uptime, and per-protocol distribution. The same counters are available to scripts via GET /api/http3/stats.

Scope tab has exactly one Save

The Scopes list pane carried a second Save button next to New, duplicating the editor pane's and saving whichever preset was selected last. It is gone; the Save in the editor pane is now the primary action color, so the button you reach for is the one that acts on what you are editing.

v0.4.36

2026-08-25GitHub ↗
Your own collaborator, no credential hunt

Connecting to your OOB collaborator used to need an api_token only you had — an agent mid-hunt would dead-end asking you for credentials to reach infrastructure you own. Hugin now enrolls itself: on connect with no token configured, it signs a challenge with this install's persistent device identity, the server mints a scoped token attributed to your device, and it's stored locally so it happens exactly once. Revocation, quota accounting, and the admin token list all treat enrolled devices like any other issued token, and reinstalling a machine automatically retires its old credential. If enrollment can't complete (server unreachable or too old), connect says so plainly instead of pretending to be connected, and `allow_local` opts into tracker-only mode deliberately.

Status tells you what you're talking to

`oastify status` now reports whether a credential is configured and whether a remote client is live, so an agent can answer "can I reach the cloud collaborator at all?" before attempting anything.

v0.4.35

2026-08-25GitHub ↗
OOB polls stop going blind when two collectors are up

Polling read only the locally-running listener even when the session's wildcard DNS pointed at the cloud collaborator, so callbacks landed on one backend while you watched an empty feed on the other — nineteen interactions sat on the remote while poll swore there were zero. Poll now aggregates both backends, labels every interaction with where it was captured, and reports per-source counts. Starting a local listener whose domain resolves elsewhere is refused outright unless you attest you know where the callbacks will land, and the check runs before anything binds.

Attaching to your own Chrome actually attaches

browser launch with attach_mode=connect ignored the value and spawned a brand-new browser with a clean profile — none of your logins, no warning. connect and attach are now accepted as synonyms of adopt, unknown modes fail loudly instead of being dropped, and an attach that finds nothing listening (or can't connect) errors out rather than silently launching fresh. navigate honors cdp_port too: it used to fall through to the default engine and fire your navigation from an anonymous profile at the target.

Batch annotations accept the ids array

flag, comment, tag_add, and tag_remove demanded a singular id even when you passed the documented ids list, so batching meant one call per flow. The array works everywhere now, and action=tag is registered as the alias everyone tries first.

Parallel cookie writes stop tripping over each other

Three cookie_jar set calls in one batch could return database-is-locked: the jar serializes its own writers but shares the database with everything else Hugin writes. Transient lock contention now backs off and retries within a second; real constraint failures still surface immediately.

Sync survives server shape changes

The OOB syncer decoded the interactions endpoint as a bare list while current servers wrap pages in an envelope, so sync died with an opaque decode error. Both shapes are accepted, pagination is followed for high-traffic sessions, and any future decode failure embeds a piece of the raw response so drift is diagnosable from the error alone.

Sessions get valid names without you inventing them

Connecting with no session name sent the literal string default, which the server itself rejects — every fresh-install connect failed until you typed an id by hand. A valid agent-<timestamp> id is generated when omitted, stored legacy defaults migrate automatically, and invalid ids are rejected locally with the rule spelled out.

Payload creation params create payloads

Asking payloads for two new payloads with a description returned ten stale ones and no error — creation params were silently ignored. Creation-intent params now mint payloads through the generate path; with no creation params it lists exactly as before.

v0.4.34

2026-08-24GitHub ↗
Host maps work again

The api_map host rollup interpolated the project id into its SQL instead of binding it, so every project-scoped hosts query died in preparation with a tokenizer error. The id is now a bound parameter, matching every other query in the store, with regression tests guarding both the SQL shape and an end-to-end round trip.

Abandoned tool calls stop haunting the connection pool

When a client gives up on a slow read it usually just walks away — no cancel notification ever arrives, so the query kept running to the ten-minute ceiling while holding database connections, and the next several minutes of tool calls failed with pool timeouts. Read and aggregate tools now carry a two-minute budget: an abandoned call is dead and its connections released before the retry wave arrives. Long-running active tools (scanner, intruder, crawler, audits) keep their full budgets.

Your facets are your engagement's

Bare list, facets, search, and doctor calls aggregated every flow on the machine — on shared-hunt setups that meant other programs' hosts filling your buckets and startup stats scanning hundreds of thousands of rows. These calls now default to the active project; pass project_id="all" when you genuinely want the whole machine.

Literal search survives bundle-heavy corpora

A body-text search materialized full response bodies for thousands of rows — on apps serving multi-megabyte JavaScript bundles that meant gigabytes per search and nondeterministic timeouts. Matching now reads at most 256 KiB per row (surfaced in the result envelope), which keeps searches inside their time budget without losing realistic token matches.

BAC reports classify their own noise

Four recurring false-positive shapes in broken-access-control audits are now auto-verified at results time: mass-assignment probes where the endpoint echoes your own data back, SPA shells served identically to every probed endpoint, replay bodies identical to baseline on public-by-design data, and telemetry endpoints that answer 2xx to anything. Genuine divergences stay unflagged, and each verdict names the evidence class that decided it.

v0.4.33

2026-08-24GitHub ↗
BAC audits stop drowning you in per-candidate noise

IDOR and parameter-mutation auditing used to fire a finding per mutated candidate: 40 near-identical entries where one would do, most of them the app's public-by-design behavior. Findings now require evidence — the body must be structured JSON (not an ack/error envelope), an anonymous control must not receive the same data, and either the guarded endpoint echoes your identity or two distinct candidate responses differ. Surviving candidates aggregate into ONE finding per locator with redacted length/status stats instead of a wall of duplicates.

SPA shells stop masquerading as bypasses

On shell-heavy apps every authenticated probe returned the same templated HTML, so verb tampering and header bypasses reported Firm/Certain hits that were just the app's catch-all page. Hugin now fingerprints each host anonymously (GET / plus a random probe path), and any response matching the shell's exact status+hash+length triple is skipped and counted — hits surface as SHELL-GATE lines in the report's partial errors, and remaining HTML-shell responses on API-looking endpoints are capped to Tentative with a verify hint.

Cancelled audits keep their results

Killing a long BAC audit (crash, restart, cancel) used to lose everything: the registry entry vanished, counters reset to zero, completed findings stayed buried in the store with no pointer. Audits are now durable — a bac_audit_runs table tracks start/progress/finish across restarts, findings persist incrementally at each wave boundary, and a reader hitting a dead audit gets the stored partial results instead of 'unknown audit'.

Audit progress shows the flow being attacked

audit_progress reported a bare requests-sent counter — against a slow host you couldn't tell whether anything was happening. Progress now names the current flow URL and its position in the queue, in the MCP tool, the REST API, and the Authorize UI.

exec_js returns values again

Every exec_js call returned null: the wrapper appended a fresh-eval nonce comment after your script, read that comment as the trailing expression, and emitted `return /*nonce*/;` — undefined. Trailing comments are stripped before the last-expression search, on both CDP and Marionette.

Clicks land on below-the-fold elements

Click measurement read raw getBoundingClientRect coordinates, so elements below the fold produced out-of-viewport points that Chromium rejected as move-target-out-of-bounds — the click failed before any drag logic ran. Targets are scrolled into view first and measured centers are clamped to the viewport; drag/type bezier mid-points are clamped to the endpoints' bounds for the same reason.

switch_frame accepts a frame index

Frame selection needed a frame_id you could only learn from list_frames output that frames don't expose. switch_frame takes frame_index (DOM-enumerated order), falls back to a numeric frame_id, and errors loudly instead of silently staying on the parent frame when neither resolves.

Session captures inherit live cookies

Building a session profile from captured flows dropped Set-Cookie headers entirely, and vault seeding missed cookies the server set during login — replayed identities ran half-logged-in. Both capture paths merge response Set-Cookie over the request cookie pair-wise (newest wins), so the profile matches what the browser actually held.

Runaway scans get stopped

A scan stuck in a retry loop could pin a resource pool slot indefinitely and survive the client that started it disconnecting. Pool guards circuit-break runaway sends, SQL statements carry timeouts, and client cancels propagate into the scan loop instead of leaving orphaned work running.

Finding triage in bulk

Marking 200 scanner findings false-positive meant 200 calls. findings update accepts filters (scan_id, check_id, severity, status, source, cwe, search) for bulk triage with a 500-item cap, and the findings action rejects mutation params loudly instead of ignoring them.

bambda sees the whole response

Lua response tables lacked top-level status/content_type/content_length/response_headers (only request-side had them), and body access gave no hint when the body wasn't captured. Response tables now carry the aliases plus a body_available flag, fall back to the Content-Length header, and error messages scrub internal filesystem paths.

The proxy stops blocking on disk

Flow persistence ran inline on the request hot path, so a slow SQLite write stalled live traffic, and dashboards re-ran rollup queries on every poll. Persistence moved to a dedicated worker, rollups gained a TTL cache, and the SQLite mmap window grew to 4GB.

v0.4.32

2026-08-23GitHub ↗
Directory fuzzing stops drowning you in SPA catch-all false positives

Wildcard calibration compared raw response hashes, so any dynamic page (nonce-stamped shells, templated SPAs) looked like unique content: probes always 'disagreed', no wildcard was ever detected, and 27/27 reported CWE findings on one engagement were false positives. Probe agreement now means same status AND (raw hash | normalized hash | length within tolerance), and volatile fragments — digit runs, long mixed tokens, hex nonces — normalize to placeholders before hashing. The old '2xx responders are never wildcards' carve-out that produced the finding flood is gone.

ffuzzer gains calibration_mode

calibration_mode=strict demands byte-identical probe bodies — deterministic on templated JSON APIs where consecutive runs flipped the wildcard verdict; auto keeps the fuzzy rule. The active mode is reported in output JSON.

api_spec discovery ignores the catch-all shell

Spec discovery baseline-probes the base path first: byte- or normalized-identical responses are the app's catch-all page, not specs, and are counted as wildcard_suppressed instead of landing in found as unknown entries.

Browser crawls run as background jobs

crawl_start/crawl_status/crawl_result/crawl_stop mirror discover's job contract (the sync crawl still works). Long crawls no longer die with a -32001 client abort at ~212 seconds while the driver kept crawling server-side.

MCP ergonomics: tool_help, aliases, projections

tool_help resolves hugin_-prefixed names against bare registry keys (hugin_bundle used to return empty docs). project accepts activate|switch|set_active, and project.list accepts fields="id,name" so wide lists fit one page instead of truncation-paging. The default response cap drops 128k to 40k chars so Hugin's output-store envelope (with its retrieval hint) reaches you before your client truncates and shows its own misleading hint.

intercept.toggle works

Every toggle call died with 'unknown toggle field': the dispatcher serialized its own action field into the handler payload and toggle's strict key validator rejected it. Dispatch concerns no longer leak into handler data.

Page-driving actions suspend the intercept queue

Only navigations suspended browser-slot interception, so exec_js, click, type_into, inject_script, fetch and reload on a driven SPA hit the live intercept queue and stalled — the silent-spinner hang. Every page-driving action suspends now; purely local ops (wait_ms, status, launch, stop, list_tabs, list_frames) don't.

Streaming responses no longer jam the resource pool

Held SSE/long-poll connections counted against max_concurrent_requests for the whole request timeout — four event streams per tab saturated the default cap of 10 and trivial parallel probes queued 16-21s behind them. Scanner probe bodies are read with a 1 MiB cap and a 2s idle timeout so a stream releases its permit immediately, and the default cap rises to 64.

scope import accepts every shape, and scope edits stop clobbering each other

Import returned 'Imported 0' for plain-string and URL-style arrays because only {"pattern": ...} objects were recognized. Parallel add_pattern/remove_pattern/update/import calls raced a read-modify-write cycle and the later persist silently dropped the earlier insert — all four mutations now hold a shared lock. URL-style patterns ('https://api.example.test/api/v1') reduce to Host or Url patterns instead of becoming host wildcards that never match a bare-host check.

v0.4.31

2026-08-22GitHub ↗
Flow lists stop crashing on legacy flows

Projects containing flows recorded by an older tool (16-character hex ids) bricked every matching list or search — 'invalid length: found 16'. The ids are now migrated to real UUIDs on startup and the reader degrades instead of dropping the page; host filters that used to error now return results instantly.

HTTPQL substring filters are instant on huge corpora

`host contains 'x'` was never parsed as a host filter — it became freetext: a full-table scan plus a literal quoted post-filter that matched nothing (and timed out past ~100K flows). The operator now means what it says, and both `host contains` and `url contains` route through FTS5 indexes (trigram host index, url phrase index) — 133K-flow corpora answer in milliseconds.

DataDome solves are measured, and captured

Slider solves used a fixed center−110 → +222 drag that missed whenever the puzzle piece offset differed (3/3 'Drag didn't solve'). The drag is now measured from the actual canvas: the piece's and the hole's left edges are found from the puzzle image via CDP. The solve browser also routes through the local proxy by default, so every request the challenge makes lands in your flow history (pass no_proxy:true only when DataDome flags the proxy egress).

The managed Chrome no longer crashes SPAs

The DOM-Invader canary was planted as an enumerable global, and Webpack-style SPAs that enumerate and redefine globals crashed V8 ('Getter must be a function') so React never mounted. The canary is now a non-enumerable property — invisible to global scanners, still readable for sink matching; operator-invoked pollution probes stay explicitly enumerable. Frame switching, DOM query contexts, cross-origin frame offsets and wait_ms gates from the same engagement batch are fixed too.

datadome.list_actions and project switch

The datadome tool accepts list_actions (per-action reference), and project management accepts switch as an alias for activate.

memory search finds findings again

hugin_mcp memory search over the scanner's finding store stopped returning results after an index reset; the index is rebuilt and a daily scheduled reindex keeps it fresh.

v0.4.30

2026-08-22GitHub ↗
The app no longer claims the previous version after an update

The updater bumped the bundle's Info.plist to the new release, then the post-update sibling sync stamped the OLD binary's version right back over it — every macOS update left the About box and Finder showing the previous version while the binary ran the new one. The post-update sync now stamps the version just installed, and the plist writer is monotonic: no process (including a stale GUI instance or an outdated headless daemon still running mid-update) can ever write a version older than what the bundle already claims. If your app already shows the old version, run `hugin update` once more — the up-to-date path re-stamps the correct version.

v0.4.29

2026-08-22GitHub ↗
Raw HTTP sends are byte-exact again

The raw-send path normalized line endings INSIDE the body too, silently growing it while the caller's Content-Length stayed stale — servers then read exactly CL bytes and answered 'Unterminated string in JSON at position N' with N == CL on every JSON POST. The head is now CRLF-normalized and the body passes through byte-for-byte; a missing Content-Length is computed from the exact body bytes (existing CL/TE are never rewritten — smuggling payloads stay intact).

Drags land inside cross-origin iframes

Drag mid-path mouse moves carried no pressed-button state, so Chromium classified the whole sequence as hover and the OOPIF under the pointer never received it — slider automation (DataDome/Arkose/GeeTest) was dead. Moves now carry the held button bit, and click/drag accept a frame_selector that offsets coordinates into the iframe's local space (resolved from the parent document — cross-origin-safe).

Bundle search works

hugin_bundle action=search rejected every call with 'Missing query' even when query was passed — the parameter schema didn't declare it. query/context/limit are now first-class params.

Project scope keeps regex patterns

Anchored scope patterns like ^manager\.example\.com$ were silently coerced to wildcards, so the exact host reported in_scope:false. Patterns now honor an explicit pattern_type (or infer regex from anchoring), and project update with scope_action errors with guidance instead of silently returning the unchanged scope.

Hugin Browser fetches https through the proxy

The in-process Boa engine's fetch agent didn't trust the Hugin MITM CA, so EVERY https navigation failed with UnknownIssuer — the browser's network was effectively dead. The agent now trusts <hugin_dir>/Hugin-Proxy-CA.pem; the Servo renderer falls back to the same PEM when HUGIN_CA_PEM_PATH is unset.

datadome.solve discovers windows over CDP

Slider solve relied on AppleScript window discovery, which fails without macOS Automation permission ('No Chrome window found' even with a GUI Chrome). A cdp_port param now adopts the Chrome and reads bounds via Browser.getWindowForTarget; explicit coordinates skip the AppleScript call entirely.

navigate reports anti-bot challenges

A DataDome/Cloudflare/Arkose/GeeTest challenge page reported status:'loaded' because its HTML passes the readiness check — every WAF-gated engagement burned a cycle. navigate now scans the live DOM and returns challenge:{vendor,kind,iframe_src} when a challenge is present.

Crawler + body-cap docs match reality

The crawler tool description lists its real actions (list/start/… — there is no 'crawl' action), and the vurl_http/get_flow body-cap contracts state the true limits and the output-store fallback for oversized responses.

v0.4.28

2026-08-21GitHub ↗
Release builds unblocked — statvfs is unix-only

The backup free-space probe compiled its statvfs helpers on every target: the macOS cross-builds hit mixed u32/u64 field widths and Windows has no libc::statvfs at all, so all three release build jobs failed on hugin-service. The helpers are now cfg(unix)-gated and width-widened via Into<u64>, and Windows ships the no-op fallback path.

Project activation now sticks as the durable server default

A session-scoped activate used to bind only the calling MCP peer, so the next fresh connection or restart re-read whatever stale default an earlier switch had persisted — scope checks silently ran against the wrong project. Activation now promotes the project to the server default (persist: false opts out), a new project set_default action sets it explicitly, and project active reports the durable default alongside the effective project.

Fuzzers no longer abort on 403 walls

A target that 403s unknown paths (gated namespaces, bot walls) silently killed forced-browse discovery after 3 consecutive 403s, stranding operators with a handful of requests per run. Content discovery now treats 403 as a legitimate response class (406/418/429/503 still abort), and the FUZZ fuzzer's default WAF strategy switched from abort-after-3 to exponential backoff with abort only under sustained blocking.

HTTP/3: fast-fail broken hosts, honest stats

The HTTP/3 stack now quickly fails hosts that don't speak QUIC instead of hanging, reports real per-host stats, and dropped the obsolete draft-32 transport.

v0.4.27

2026-08-21GitHub ↗
Crawler trusts the Hugin proxy CA on every TLS client

A deep crawl against a Hugin-proxied HTTPS host could fail with UnknownIssuer unless accept_invalid_certs was set — the MITM root was trusted for some clients but not others. Every verify-on client now trusts the proxy CA, so proxied TLS crawls work out of the box.

3 pre-existing clippy warnings cleared

The fetch_and_extract too-many-lines and backup.rs statvfs byte-widening warnings are gone — no suppressions, the code was restructured to genuinely satisfy the lints.

v0.4.26

2026-08-21GitHub ↗
Oastify outbound-delivery proof now surfaces

Auto-connect built the payload tracker and syncer but never armed the remote client, so the interactions view returned empty even when the OASTify server had received DNS/HTTP callbacks — outbound-delivery proof silently looked dead and operators fell back to external observers. Auto-connect now creates the remote client, binds the session, and surfaces callbacks through the app.

v0.4.25

2026-08-20GitHub ↗
14 harness fixes from the engagement batch

Discovery now validates unknown builtin wordlists instead of silently running a 0-word job; the crawler reconciles zombie sessions, trusts the MITM CA by default, and applies per-host backoff with Retry-After / skip-on-429 so deep crawls stop timing out; vurl_http and repeater scope evaluation now honor URL patterns and scope_override; bambda filters are nil-safe on responses; cookie jar search values are shown hash-safe and whole so hand-copying no longer mangles them; and more across the browser, events, api_spec, session_notes and bundle surfaces.

UI parity for the 7-day backend patch wave

Backend/MCP-only surfaces are now exposed in the app: the crawler status shows the auth-effectiveness verdict (an auth-ineffective crawl warns its flows are anonymous and not safe for BAC/replay) plus proxy-truthful flow counts; the scanner coverage shows per-payload WAF-blocked counts; the proxy settings indicator shows the actual bound port when it differs from configured; scheduled backups skipped for low free space are surfaced; and the bundle engine's search + mine actions gained REST routes and GUI controls.

FFuzzer flow capture + backgrounded runs

FFuzzer can now opt into persisting every probe as a flow so passive analysis / nerve / comparer / send-to-scanner see the corpus, and very large wordlist runs auto-background with a friendly notice instead of appearing to hang.

v0.4.24

2026-08-20GitHub ↗
Crawler now fetches edge-fronted hosts and reports honest counts

Edge networks broke the crawler's plain HTTP engine and made its accounting lie — requests made but '0 crawled'. The engine now falls back to HTTP/1.1 + env proxies when the edge demands it, and crawl status reports the proxy-truthful flow count for the crawl window, so a proxied crawl shows what it actually captured instead of zero.

Crawls that intended auth but got none are flagged, not silently trusted

A crawl launched with custom_headers that comes back on login redirects / 401 / 403 is now marked auth-ineffective in REST and MCP status — the same verdict session-cookie crawls get from the auth proof. Anonymous flows can no longer be fed to BAC/replay with no visible gate.

exec_js/fetch return-value injection fixed

Fresh-eval scripts that returned objects (fetch().then(r => ({...}))) came back null in the MCP result because the return-wrapping bridge dropped the value. Fetch and exec_js results now round-trip the injected return value correctly.

Bundle 'mine' action lands in MCP

The bundle mine_deminified_endpoints capability is exposed as a first-class MCP tool action — mine a JS asset for endpoint extraction and get the newly discovered endpoints back.

Browser cookie-backend targeting, bambda table literals

Cookie operations now target the explicitly requested backend (active vs current) instead of always the active one, and bambda test/save_preset accept bare table literals and expressions.

v0.4.23

2026-08-20GitHub ↗
Per-account Oastify tokens — each Pro user owns their OOB callbacks

The desktop previously used one shared api_token for every account, so all OOB sessions shared a single owner (no isolation, no per-user revocation). A Pro account now mints its own scoped, TTL-bounded Oastify token from hugin-web on connect, automatically. The shared config token is only a fallback when no account/broker is reachable.

Cookie jar attaches to direct vurl sends

vurl HTTP sends through the service now auto-attach the canonical cookie jar (RFC 6265 §5.4) so authenticated state carries into vurl-driven requests. The raw byte-for-byte send path stays unmodified to keep request-smuggling probes intact.

Tunnel domains are per-project now

Scope tunnel settings used to mutate one process-wide list shared by every project. They're now stored and consulted per active project (HTTP and HTTP/3), so each project's MITM-bypass set is isolated.

Scope tab gets a Save button in the left pane

You can now Save a preset (or create a new one from the current form) straight from the scope list header, and Apply-to-Live works even without a preset selected.

Browser sessions no longer die mid-login under load

A single slow cookie round-trip on a heavy SPA (websockets, trackers, deep DOM) used to mark the whole browser 'stalled' and evict + relaunch it — losing the just-completed login. The stall now requires consecutive timeouts before eviction, so a healthy busy browser stays up through the login POST.

v0.4.21

2026-08-19GitHub ↗
The Sitemap now shows the project you're actually working in

The UI booted with no active project and never synced the daemon's selection, so the header could say 'All Projects' while the Sitemap only listed the daemon-active project's hosts (and vice versa). The active project is now fetched from the daemon at startup and shown as a scope badge in the Sitemap header — you always see which project's hosts you're looking at. Project activate/deactivate that fails server-side now surfaces the error and reverts the selector instead of silently doing nothing.

Auto-backup can no longer fill the disk and take the app down

A backup runs VACUUM INTO — it needs roughly 2x the database's size in free space. On a nearly-full volume the copy died mid-write, leaving a partial backup file, and every subsequent database write failed with 'No space left on device' until the app stopped. The backup task now checks free space before starting and skips with a warning when the volume can't hold the copy, and stale partial backup files from crashed attempts are cleaned up automatically.

endpointer accuracy fixes

The endpointer audit fixes land: probes now record the method actually sent (not the flow's original method), static-path detection strips query/fragment before extension analysis, successful probe snapshots are distinguished from rows that included errors, API endpoints no longer undercount their content types, host extraction keeps the port so bundle/flow keys merge, and snapshot skips stored paths without a leading slash are counted.

v0.4.20

2026-08-19GitHub ↗
exec_js scripts with await no longer crash Chrome with Illegal return

Fresh-eval scripts (browser exec_js / page script execution) were wrapped so a `return` landed outside a function — any script containing `await` failed with 'Illegal return statement' even though the same script ran fine in a page context. The fresh-eval marker is now appended at the END of the script, so the return-wrapping detection in the Chrome bridge sees the real script shape and async scripts evaluate correctly, including `fetch().then(async r => ({...}))` chains.

Browser automation lands on the tab you asked for

Right after launch the wrong tab could end up active (a blank chrome://new-tab-page slot), so navigate / exec_js / screenshot acted on an empty tab while the real page sat one tab over. Launch now activates the seed-host tab and closes the stray blank tabs it created. Tab fixup runs ONLY on fresh launches — adopting an existing Chrome session leaves your tabs exactly as they were.

Auto-relaunch no longer navigates to a null page

When the browser session dropped and Hugin relaunched it, the relaunch re-navigated to about:mullvad-browser (a null origin), surfacing a misleading network error. Relaunch now only re-navigates when the current page is a real http(s) URL, and otherwise reports the relaunch cleanly.

Proxy listener reporting shows the REAL bound port

settings proxy_listener_get / proxy_listener_status now surface the actual bound port alongside the configured one. When the config port is OS-assigned or was rebased at bind (the 18085-vs-53335 class), probing the live listener reports 'listening' instead of a phantom 'port_in_use', and the mismatch is flagged so routing failures are diagnosable at a glance.

Stuck browser sessions report 'stuck', not 'already running'

A session wedged on Chrome's error/blank page reported already_running — correct process-wise, useless operationally. Launch now detects the wedged-tab state and reports status='stuck' so the recovery path (relaunch / reset) is obvious.

v0.4.19

2026-08-19GitHub ↗
Browser navigation now verifies the page actually loaded

navigate / browse / launch no longer report success while the tab is stuck on Chrome's error page (chrome-error://chromewebdata/). Every navigation is verified against the final document URL; a stuck tab is hard-recovered once (fresh Page.navigate keyed on URL change, not the load event) and, if still wedged, the actual Chromium net error (ERR_...) is surfaced instead of a fake 'loaded'. The launch path retries the seed URL once after confirming the proxy is up, and marks the launch failed if the page never loads. exec_js now flags when the empty result is because the tab is on an error page, and screenshots do a liveness pre-check before capture so a wedged tab surfaces immediately instead of hanging 30s.

Saved wordlists are now first-class in discovery

discover, param_discover and ffuzzer all accept a wordlist_name resolved from the managed wordlists store — so a curated list saved via wordlists.save can be fed to any discovery tool by name instead of pasting the whole array inline. Inline wordlist still takes precedence; extra_wordlist + JS mining still extend a named list.

batch_raw_send honors the session/project-effective scope

Batch raw sends to in-scope hosts under a session-scoped project no longer falsely report out_of_scope. The flow-persistence gate now resolves the same session/project-aware effective scope as hugin_vurl_http / hugin_scope action=effective, so a wildcard include that lives only in the bound project's scope is honored instead of the global snapshot rejecting the in-scope flow insert.

paramhunter output is bounded; multibyte crash fixed

A popular parameter no longer balloons a finding to hundreds of full request URLs — distinct samples are capped (the true total stays in count). A value whose byte length exceeded the truncation cap but whose char count was within it no longer panics (char-aware truncation). Host-based analyze now routes through full captured flows so query params are no longer silently dropped.

v0.4.18

2026-08-18GitHub ↗
Bundle capture over already-crawled traffic (Stored source)

The bundle engine's Stored capture source — reuse JS already captured in the store (proxy / crawl traffic) for a host, no re-fetch — is now reachable from the UI, REST, and MCP bundle capture via mode=stored. Previously only the recon pipeline could use it. The natural 'extract all details from what I already have' flow now works from any surface.

Raw JS asset bodies retrievable

A new bundle asset-body action (UI / REST / MCP) returns a stored asset's raw captured bytes (base64) plus metadata — the ground truth the deminify/deobfuscate pipeline transforms. Use it to inspect bytes the deminifier mangled; the UI source viewer gains a Raw button.

Virtual-host capture + deminify session hydration

Host-gated targets behind a bare-IP seed now capture correctly: the vhost chain's scope gate accepts seed OR vhost authority (host-identity only, ports ignored), and the connect port falls back to the Host-header port / seed URL port / scheme default instead of hardcoded 443. Deminify hydrates the capture config + reachable origin from the asset's session so source-map fetches land on the right host.

Scope presets surface persistence failures

The Scope tab's preset save/duplicate/delete no longer swallow disk-write errors silently — a failed write now rolls back the in-memory state and surfaces a toast/inline error instead of showing a preset that vanishes on restart. Load failures are logged instead of returning an empty list.

v0.4.17

2026-08-18GitHub ↗
BOLA path-segment IDOR now confirms end-to-end

The standalone active-bola check could never confirm the canonical REST shape /api/Users/:id. Two gaps: the executor's differential gate suppressed value-only JSON diffs (two profiles differing only in email/id/role score ~0.5-0.7 leaf-Jaccard, so every id-swap probe was silently dropped), and path insertion points named path[i] never passed the id-parameter filter. A dedicated BolaIdSwap trigger now fires on any 2xx body that differs from the clean baseline (the analyzer decides foreign-user via PII heuristics), and path-segment IDs classify as id points. Verified live against Juice Shop v20: /api/Users/1 now yields 'BOLA - Unauthorized Object Access' (Certain, confirmed-cross-user-access), alongside the existing BAC suite's CrossIdentityEcho / path_param_mutation / verb_tampering / FunctionLevelAuthz confirmations.

Browser-authenticated attacks land across SQLi, SSRF, RCE, XSS + uploads

The four offensive-browser capabilities are now production: browser-authenticated SQLi (SqliTransport), SSRF (BrowserSsrfSender), RCE delivery (browser_rce), and reflected/stored XSS with browser-execution verification (ReflectedXssVerifyStore + drain, context-aware payload selection, prototype-pollution OOB gadgets, stored-XSS second-order, postMessage AST origin validation). Upload bypass transports, download_steal, and network_log round it out — every probe rides the real cookie-jar + TLS fingerprint, so login-gated targets that 302 the reqwest scanner now get tested.

SPA attack driver + stack-agnostic authz-bypass audit

Drive a headless browser through a React/Vue/Angular/Next app to capture every XHR/fetch and RSC flight payload as flows, then replay each guarded/hidden route and disabled button directly server-side (client checks stripped) — flagging any that return 2xx as cosmetic protection. spa_authz_bypass is the client-side authz gate, IDOR, and vertical-escalation sweep behind it.

DOM-XSS: real JS AST taint walker + Burp DOM Invader parity

A genuine JS AST taint walker tracks source→sink data flow through minified bundles (with sandboxed source→sink probing that needs no browser slot), plus extended sinks, active postMessage origin tests, form canaries, and opt-in eval instrumentation. Bundle extraction is deobfuscated first so hidden endpoints get discovered.

Sequential ID swap corpus for IDOR + Relay Global-ID BOLA traversal

The IDOR check now walks ±10 around every captured numeric ID, and GraphQL Relay Global-ID (node(id:)) BOLA traversal encodes Type:localId base64. HS256 weak-secret dictionary expanded; DOM-XSS same-block pairing added.

Open-redirect hardened with WHATWG-faithful host parsing

host_equals_or_ends_with now mirrors the WHATWG URL parser: userinfo, protocol-relative, extra-slash, and backslash redirect shapes all classify correctly, with IPv6 bracket handling. Open-redirect is wired into both pipeline orchestrators.

SQLi: UNION data-exfiltration + OOB/second-order correlation

UNION payloads now pull actual data (schema, credentials) out of targets and are labelled SQLI-005 distinctly; stacked queries are SQLI-006. OOB and second-order SQLi correlate against the real Oastify callback domain.

Flagged flows no longer lose their responses; probe HTTP version recorded

A proxy/vurl bug dropped responses off flagged flows (silently breaking passive analysis of them) — fixed. The scanner now records the actually-negotiated HTTP version per probe so HTTP/2-specific checks gate on the protocol the attack bytes really travelled on; RFC 9220 WS-over-HTTP/3 wired.

OAuth parser-differential attack battery

A template-expanded battery (path confusion, userinfo, backslash, double-decode, OPP) drives the OAuth redirect_uri parser against the target's REAL registered callback instead of a hardcoded host — Authentik CVE-2024-52289-class checks included, surfaced as an MCP analyzer tool.

BAC: two-account auto-setup orchestration + async audit

auto_setup orchestrates the two accounts BAC/IDOR audits need; audit_async runs long sweeps without hanging the MCP call. FunctionLevelAuthz toggle exposed on all surfaces.

v0.4.15

2026-08-17GitHub ↗
Scope tab: presets live in the left pane — no more duplicate empty state

The scope tab's right pane no longer renders its own empty states ('No scope presets' / 'No preset selected'). The left pane's preset list already owns preset selection and the no-presets state; the right pane is the scope editor form for the selected preset, or the live scope + project metadata when nothing is selected.

Browser-authenticated scans no longer abort on a single 403

The edge-403 guard that stops a browser scan when the session dies was over-eager: ONE 403 on a single probe response (an sqli baseline or any ssrf payload) aborted the ENTIRE scan globally, even when the endpoint was genuinely 403-gated or a WAF/internal service merely refused one mutated payload. Both scanners now abort only on the uniform signature — consecutive 403s on an endpoint that previously answered (captured original status was not 403) — and genuinely forbidden endpoints are skipped per-flow instead of killing the scan.

Stale-tab cleanup can no longer close the operator's tab

ensure_fetch_tab validates that the tab still at the parked index is actually on the parked origin before closing it; if the operator closed the parked tab, the tab that slid into that index (possibly their own) is left untouched.

Host deletes are project-scoped on every path

MCP flows delete_by_hosts without a project_id used to delete the host's flows in EVERY project — the same cross-project wipe the sitemap deletes fixed on the UI side. It now defaults to the active project, mirroring pipeline/api_map.

Async pipeline runs no longer leak memory

Finished detached pipeline runs stayed in the in-process registry forever, one full report per run_id. They are now evicted 30 minutes after completion, bounding the map in long-lived daemons.

GeeTest solver configure path fixed

The external-tool configure() path rebuilt the subprocess runner with the harness .py as the executable; it now keeps the python interpreter on the solver and rebuilds python3 <harness> exactly like the builder does.

v0.4.13

2026-08-16GitHub ↗
Pipeline runs detach into background async tasks

recon_pipeline and other long pipeline runs now survive the originating MCP call's timeout by detaching into a background task (RunSvcParamsOwned + PipelineRun::from_owned). bac audit_async is Scan tier again (was falling to the Admin default), paramhunter reconstructs the full URL from FlowSummary host/path/scheme, dashboard_dispatch gained an 'overview' aggregate action, and fresh-scan request counters are seeded so deduped/OOS scans report 0 instead of leaking the previous scan's totals.

Antibot fixes: site_key was dropped, reCAPTCHA Enterprise minting added

hugin_antibot recaptcha_token silently rejected site_key (and action_name/timeout_ms/captcha_type/site_url/enterprise_payload/provider/raw_body/proxy_port) because AntibotParams lacked them — all wired through typed deserialization. captcha_solve now handles reCAPTCHA Enterprise via a token-minting path (captcha_solve_recaptcha) that calls grecaptcha.enterprise.execute()/execute() in page context and loads the script on demand, routing recaptcha_v2/recaptcha_enterprise explicitly or by auto-detection.

Browser input: shift-aware key dispatch

Shift-modifier chords (Shift+letter etc.) dispatch correctly instead of truncating/duplicating type_text — modifier state is now tracked through the whole chord.

Sitemap, import and api_map scoping fixes

Sitemap scanner issues are project-scoped (no cross-project pollution), host deletes work, tree flicker is gone and the requests pane is path-scoped; imported flows are stamped with project_id with a raised import body cap; api_map scopes its flow fetch to the active project.

Browser scans: 120s deadline + no OS focus steal

browser_sqli/browser_xss no longer hang (default scan deadline 120s), and SSRF/RCE/SQLi/XSS browser scans stop stealing OS focus.

v0.4.12

2026-08-16GitHub ↗
spa_authz_bypass no longer no-ops on proxied targets

The audit now skips CONNECT tunnel-setup handshakes (every proxied target stores them), so it replays real spa-api/xhr/non-GET endpoints instead of only returning "'CONNECT' HTTP method is unsupported" Info probes. An explicit-URL probe can also be tested as a real finding via guarded:true with method/headers/body.

5 live-tested 0.4.1 bugs fixed

Scope-session handling, scan hangs, header naming, and empty-asset discovery are corrected; regression coverage added for the asset-discovery and open_redirect deadline fixes.

GeeTest solver integration in vurl

The vurl engine gained a GeeTest solver (geetest.rs modules) for captcha-gated targets.

Browser cookie-seed + scan hardening

cookies_seed falls back to Network.setCookies on the active page when Browser.setCookies is unsupported; browser scans abort cleanly on edge-403, Chrome before-script list/remove works, and the scan param contract + reload action are fixed.

v0.4.11

2026-08-16GitHub ↗
cookies_list can reveal full values, gated behind admin

A new `reveal` param (plus the existing `HUGIN_BROWSER_COOKIES_FULL=1` env opt-in) surfaces full cookie values including `HttpOnly` ones — but only when the caller holds admin permission. The disclosure gate fails closed: default-deny still masks every name and tags session-shaped names (SESSIONID/JSESSIONID/PHPSESSID/csrf…) with a `<session>` marker, so a passive caller cannot read session values.

cookies_seed reloads the page so seeded cookies take effect

After seeding cookies the active page now reloads (Chrome via CDP `Page.reload`, Mullvad/Servo re-navigate to the current URL). `HttpOnly` cookies are never reflected in `document.cookie` and in-flight requests didn't carry the fresh session, so without the reload the dashboard stayed a stub. Best-effort: a reload failure is reported but does not fail the seed.

discover no longer suppresses uniform-2xx hits as a wildcard

A controller that answers a uniform 200/204 body to every path (e.g. a method-gate) is no longer misclassified as a suppressing wildcard/soft-404, which previously hid every real endpoint and made discover report 0. Only a uniform non-2xx error/redirect status suppresses matches.

dom-xss test de-flake

Fixed the `bundle_url_without_ast_flow_stays_quiet` test flaking under parallel execution (shared process-wide evidence store + colliding URL) — it failed the v0.4.1 release gate.

v0.4.1

2026-08-15GitHub ↗
browser_sqli now completes within the MCP timeout

browser_sqli defaulted a 5s server-side SLEEP into every time-based SQLi payload, which multiplied across flows × params and blew past the 120s MCP cap on every multi-flow scan. It now defaults to a 1s delay (matching browser_rce and browser_ssrf), and the sender measures real elapsed time so time-based verdicts are reliable.

get_tls_profile is no longer behind the TLS-override gate

A read-only TLS-profile fetch no longer requires the HUGIN_BROWSER_TLS_OVERRIDE=1 dangerous-op opt-in that was meant for TLS-override writes.

browser-authenticated reflected-XSS + upload transports

The browser transport now carries reflected-XSS execution-verify and multipart file-upload scanning, plus multipart parameter discovery across flows.

Browser-attack fixes and hardening

Closed a dom_xss_replay_verify flow_id/drain scope bypass; fixed download_steal being gated behind the wrong license action; SPA authz-bypass no longer flags every authenticated 2xx as client-side-only (adds an unauthenticated control replay); browser_fetch is no longer CORS-blocked cross-origin (proxy injects permissive CORS on stamped requests); drained #[allow] debt and extended the no-allow gate to #[expect].

v0.4.0

2026-08-14GitHub ↗
Browser-authenticated scans: SQLi, RCE, SSRF delivered through a real browser

The scanner can now run SQL injection, command/RCE, and SSRF checks through a live browser session instead of raw HTTP only. `browser_sqli` (via SqliTransport), `browser_rce` (cmdi/ssti/deser delivery bridge), and `browser_rce`'s SSRF sibling (BrowserSsrfSender) authenticate like a real operator, ride the cookie jar, and persist findings with scope+budget gates and a default Chrome browser.

Browser execution-verify for reflected/stored XSS + postMessage AST origin validation

Reflected and stored XSS findings are now confirmed by actually executing in a browser, with context-aware payload selection and a prototype-pollution OOB probe. Stored-XSS second-order render-location fetching reconstructs where a payload was written. A new postMessage AST walker validates `e.origin` guards and classifies Strict / Weak / None, demoting library bundles and suppressing false positives on strict guards.

Cookie-jar bridge: browser auto-seeds and auto-harvests cookies

The browser now seeds cookies from the jar before a fetch and harvests them back, so a jar-only session cookie is actually sent and a cookie the fetch sets is tracked — closing the gap where a browser-authenticated request silently lost its session. Mullvad-browser mode gained W3C GetCookies/AddCookie support.

SPA attack driver + spa_authz_bypass

A new SPA driver captures XHR flows, detects React Server Components, harvests state blobs, and drives interactive exploration; `spa_authz_bypass` is a stack-agnostic client-side authz-bypass audit. Browser-harness gaps closed: dialog auto-dismiss, auth, downloads, UA override, frame switching.

OAuth parser-differential attack battery

A new active battery feeds OAuth parsing variants (token/payload normalization differences) to a differential analyzer, surfaced as an MCP tool, with a hardened test battery.

Flow capture wired across tools so passive analysis sees real traffic

Intruder, Discover, and FFuzzer opt-in flow capture so their hits feed passive/nerve/comparer/scanner analysis; replaying a sequencer flow persists it for passive analysis; the crawler emits rendered-DOM flows so SPA/WAF post-JS content reaches passive checks.

Scanner & protocol fixes

Actually-negotiated HTTP version recorded across all consumers; RFC 9220 WebSocket-over-HTTP/3 wired; robots.txt opt-in by default; blind-vuln/invasive classes no longer silently skipped; persistent CORS findings; redirect-based BOLA verdicts; sandbox-degrade DOM-XSS hook.

v0.3.0

2026-08-13GitHub ↗
Scanner findings now actually persist on every scan surface

A systemic audit found that several agent- and MCP-facing scan paths called the scan engines and returned findings as JSON but never wrote them to the store — the operator saw 'scan complete: N findings' while the Findings view stayed empty. Fixed across the agent pipeline, recon pipeline, workflow scan steps, offensive-scan tools, and the MCP sqli/xss/pathtraversal/open_redirect/authz scan handlers, plus the MCP pipeline, scanner rerun, and bundle auto-scan. Every scan now lands its findings in the store for triage, collab, and reporting.

BAC/IDOR: two-account auto-setup + real engine-side replay

BAC audits now orchestrate two-account setup automatically, and AuthZ/IDOR replay runs in the engine with time-based SQLi confirmation, BAC concurrency, deeper triage, and chain correlation. IDOR scans walk a sequential ID-swap corpus (±10 around captured numeric IDs) and session-profile headers are set for classic scans.

DOM-XSS: real JS AST taint walker + sandboxed source→sink probe

Minified bundles are now walked with a real JS AST taint analysis, and a sandboxed source→sink probe confirms execution without needing a browser slot. Same-block pairing and an expanded HS256 weak-secret dictionary round out the scanner.

Open redirect: semantic detection + full bypass corpus

The active check now detects DOM taint, header-driven, chain-following, canary-agnostic, OAuth redirect_uri, and body/header params, routed through the full shared bypass corpus — and is wired into both pipeline orchestrators.

SQLi: OOB + second-order correlation, distinct UNION/stacked labels

Blind SQLi callbacks now correlate through the real Oastify domain, second-order SQLi findings surface, and UNION (SQLI-005) and stacked (SQLI-006) findings are labeled distinctly. Path-traversal OOB also routes to the real callback domain.

GraphQL: Relay Global-ID BOLA traversal

The scanner now traverses Relay `node(id:)` Global IDs (Type:localId base64) for BOLA, plus a GraphQL Relay node() bypass probe.

Antibot: hCaptcha + Cloudflare Turnstile solve

Cross-origin frame-switch exec_js solves hCaptcha and Cloudflare Turnstile; checkbox captcha_solve is self-contained (launch+navigate, no key needed); GeeTest success detection + shadow-DOM slider.

Intruder: all vuln-class payload corpora as builtin wordlists

The remaining 24 vuln-class payload corpora (plus the open-redirect bypass corpus) are wired as builtin wordlists and agentic payload sets.

Workflows: live run-completion subscription

Passive runs now appear in the Logs pane live via a run-completion subscription, and the UI list live-refreshes on refresh_trigger.

Egress/ratrace/intercept fixes

Rate-limit rotate-and-retry escape, api_spec local file, crawl 0-page diagnostic; ratrace divergence evidence compares only successful responses (kills false DirtyCatch on all-failed bursts); intercept backfills observe-mode captured status.

v0.2.37

2026-08-12GitHub ↗
Output store: id= is a hard scope, not a ranking boost

`hugin_output_store action=search` accepted `id=` but applied it as a ranking preference — so a query term present in multiple outputs (e.g. `perPage`, `products`) still leaked rows from other outputs (pipeline_, bac_audit_), and could push the target output's own rows off the page. `id=` is now a hard filter across all three search paths (FTS5, filter-only, content-substring fallback): only rows from that exact output id are returned, ever. This unblocks triaging a specific stored tool result (like a 40-item IDOR scan) without paging raw JSONL.

Oastify: native view live-polls + DNS-mismatch warning covers oastify.one

The native OOB server panel now live-polls every 2s while the server runs, so callbacks land without a manual tab-switch (it previously only refreshed on mount/tab change). The native-start DNS-mismatch warning now also fires for `oastify.one` (both it and `oastify.eu` resolve to the remote VPS, not your public IP) and points to `oastify:connect` for the cloud-hosted OOB case — so a fake domain like `oastify.local` + an IMDS IP no longer silently yields a dead OOB surface.

v0.2.36

2026-08-12GitHub ↗
Workflows: live run + list updates

The Workflows tab now reflects external changes live. The workflow list subscribes to the global refresh trigger, so a workflow created/enabled/disabled via MCP or REST appears without a manual action. The Logs pane subscribes to the engine's run-completion bus, so background passive runs land in the history in real time instead of only after a manual action or pane toggle.

Egress: rate-limit rotate-and-retry escape

The vurl_http per-host 429 cooldown (Retry-After guard) now has a companion escape: `settings rate_limit` (list/clear) exposes the tracker, so after switching egress (new relay/proxy/direct) you can clear the stale cooldown and re-fire immediately instead of waiting out a window tied to the old IP. This is the fix for CF-1015-style lockouts.

api_spec: docs-to-spec + local-file ingest

Many targets (Paddle, Astro/Docusaurus/Redoc) expose no machine-readable spec — only HTML API-reference pages + a nav JSON. A new `docs_to_spec` action extracts an OpenAPI-shaped endpoint/param map from crawled HTML or a nav JSON, feeding api_map / bac_audit / param_discover / seed_corpus. `parse` also accepts a local `file_path` for oversized/out-of-scope specs (e.g. the 7.4MB paddle-openapi).

Intercept: Code column shows real status

The Intercept view's Code column was empty in observe mode because capture ran at request time, before the response existed. The observe-mode ring entry is now backfilled with the real status/size/content-type once the response arrives — parity with HTTP History.

Browser: configurable crawl timeout + exec alias

The crawl SPA-stability poll had a hard 30s cap that killed crawls on heavy Astro/SPA pages. `nav_timeout_ms` makes it configurable (0 disables), and a timeout surfaces as a retryable condition instead of a silent fail. `browser` also accepts `exec` as an alias for `exec_js`.

RatRace: divergence evidence only on successful responses

A burst where every request failed no longer fires a false DirtyCatch — divergence evidence now compares only successful responses, so an all-failed burst is reported as failed, not as a race.

v0.2.35

2026-08-12GitHub ↗
BAC: kill the false-positive factory

Legacy APIs that return HTTP 200 + `{"success":false,"error":{...}}` for every credential pair (e.g. Paddle's `api/2.0`) were counted as 'authorization enforced identically' — two byte-identical denied responses fired 63 false high/critical findings. The engine now recognises structured error envelopes and never treats two error bodies as an authz success. Cross-identity echo also rejects the synthesized anonymous baseline (comparing an authed identity to the attacker's own capture is meaningless), and verb-tampering / echo findings on tiny static bodies (robots.txt, empty settings) are demoted to Tentative.

BAC: per-endpoint budget + coverage warning

A single noisy endpoint could consume the whole request budget and silently starve the rest of the sweep (21/200 flows processed, no signal). Each endpoint now has a 40-request cap, and a run that finishes with `processed_flows < total_flows` surfaces a COVERAGE warning in the report.

BAC: per-check skip telemetry

A check that sent 0 requests was indistinguishable from 'corpus didn't trigger it' vs 'gates filtered every flow out'. Each check now reports `skipped_flows` + `skip_reasons` (code/detail/count), so a 0-request row explains why.

BAC: triage by audit run + real finding ids

The audit report's inline findings showed `id:0` while the DB-backed list showed real ids — the list and the status-mutation endpoint disagreed on what identifies a finding. Findings are now stamped with their persisted row id, and `triage_set_status` accepts an `audit_id` to bulk-triage an entire FP run in one call.

Decoder: URL-safe base64 + smart-decode URL pre-pass

Base64 decode now tolerates the URL-safe alphabet (`-`/`_` + padding restore), so `/sdk/evalx/{context}` paths decode instead of 'Invalid padding'. Smart-decode URL-decodes the input up front, so a URL-encoded base64url context no longer returns 0 layers.

Bundle status, browser exec alias, scan 0-flow explain

`bundle` gains a one-round-trip `status` action. `browser` accepts `exec` as an alias for `exec_js`. Path-traversal scan explains `flows_scanned:0` when flows matched the filter but none had file/path-shaped params — no longer misread as a clean result.

v0.2.34

2026-08-11GitHub ↗
Authorize: BAC UI↔backend parity — findings to RatRace + seed corpus from spec

The Authorize view now exposes two backend actions that were previously MCP/REST-only. `to_ratrace` adds a Send-to-RatRace button in the findings panel — each qualifying finding's replay flow is converted into a race-condition candidate. `seed_corpus` adds a 'Seed from spec' dialog in the BAC Intelligence → Corpus tab that pastes an OpenAPI/Swagger body and extracts example values into the ID corpus for IDOR auto-detection. Both route through the same bridge the MCP + REST surfaces use.

Concurrent hunting sessions no longer collide

Two MCP sessions on different targets used to fight over a single browser slot (the `BrowserMap` was keyed by one shared proxy port, so the second session reused the first's browser and bled scope + cookies). Each MCP peer now gets its own per-peer relay port that tunnels to the shared proxy — distinct browser slot per session, with flow capture intact.

Updater: silence CDN DNS false-positive

The updater's DNS-consistency check compared exact IP sets, so GitHub/Cloudflare round-robin (system resolver landing on `…121.5` vs DoH on `…121.6`) fired a scary 'possible DNS hijack' warning on every update. A /24 prefix fallback now recognises adjacent pool members as consistent; the Ed25519 signature remains the hard gate.

Browser: byte-exact POST body through browser_port

Repeater requests routed through the browser no longer re-encode the body as UTF-8 — JSON with unicode/binary was getting a wrong Content-Length and a 'invalid JSON' 400. The body is now always sent as base64/Uint8Array, so framing matches the original bytes.

v0.2.33

2026-08-11GitHub ↗
GeeTest slider: success detection + shadow-DOM piercing

`captcha_solve` now recognises a solved GeeTest via the `.geetest_success_show` overlay (visible) and `.geetest_box` (hidden) — the legacy check only looked for `.geetest_holder`/`.geetest_panel`, which Tapbit's GeeTest does not use, so a solved slider was never detected as gone. The slider locator also walks open shadow roots (GeeTest / some DataDome render the handle inside a shadow DOM where `document.querySelector` returns null), and the `browser` drag action accepts `from_x`/`from_y` as aliases for `x`/`y`.

Bundle: deminify consumes source maps, per-session assets

`deminify` now decodes inline `data:` source maps (base64 + percent-encoded) and logs when a map fetch/reconstruct fails instead of silently returning minified beautify. A new per-session asset-membership table fixes `assets?session_id` returning `[]` on re-captured hosts (assets were deduped to the original session). `follow_chunks` is exposed so Vite/webpack lazy chunks get fetched.

Crawler: redirects no longer counted as errors

A redirect chain that terminates on a 3xx (off-scope hop, loop, or max-redirects) is no longer recorded as a crawl error — Netlify/CloudFront hosts that 200→redirect showed `errors == visited` and `urls_crawled == 0`. Only real transport failures count now. `js_analysis` (on by default) also auto-enables headless rendering so Vite/React SPA client-side routes are discovered.

vurl_http: auto Content-Type for JSON bodies

A JSON object/array body sent with no explicit content-type now gets `Content-Type: application/json` automatically — strict JSON APIs were rejecting the POST with 400 until the header was set by hand.

v0.2.32

2026-08-11GitHub ↗
HTTP: 429 rate-limit backoff + loop-guard

`vurl_http` now honors `Retry-After`, auto-backoffs, and refuses to re-fire a host inside a rate-limit window (both HTTP and raw-socket sends). Previously a caller hammering a rate-limited endpoint reset the window every request (observed 556s→1s spiral); now a limited host reports the remaining wait and blocks until it elapses.

Browser: fresh-eval guard for exec_js

`exec_js` prepends a unique nonce so repeated/identical scripts can never return a cached result — the engine re-parses every call, killing stale-eval bugs that silently corrupted crypto/recon debugging.

CAPTCHA: auto-detect Turnstile/hCaptcha + navigate solves it

`captcha_solve` auto-detects the live captcha kind (Turnstile/hCaptcha vs slider) when no `captcha_type` is given, and `browser navigate` detects + solves a Cloudflare Turnstile challenge in-page — turning CF-gated dashboards into usable sessions instead of ERR_TIMED_OUT dead-ends.

DataDome: OS-input solver ships in every build

The slider/audio solve path (enigo-based) is no longer gated behind `--features dev-bypass` — it's a normal Pro feature. On a headless host `enigo` fails at runtime with a typed error instead of a compile-time block.

Pipeline + BAC: auth identities by profile, longer audits

`pipeline` `auth_contexts` now accepts session-profile ids/names (strings) and resolves them to captured headers; `bac_audit` defaults to a 15-minute deadline so large two-identity audits don't time out mid-sweep.

UI parity: payload corpora + bundle pagination in the GUI

The Wordlists Browse tab now surfaces all 25 server-side vulnerability-class payload corpora, the Intruder payload picker gained a 'From corpus' dropdown, and the Bundles view paginates endpoints/secrets (REST + UI support `limit`/`offset`/`has_more`).

v0.2.31

2026-08-10GitHub ↗
Browser: pre-load scripts now apply on the FIRST navigation

`inject_script` (the `Page.addScriptToEvaluateOnNewDocument` primitive) previously only hooked the *second* navigation — the injected boot marker (e.g. a `window.ethereum` signer or provider hook) was undefined on the very load that needed it. `navigate` now registers before-scripts BEFORE issuing `goto`, so pre-load injection runs before the target SDK boots on the first load. This is the primitive that lets an agent inject a signer/provider at document-start so a session-gated app's native login mints a session carrying the anti-bot binding.

Browser: inject_script discoverable + correctly documented

The `inject_script` / `remove_script` / `list_scripts` actions are now listed in the browser tool docs and the action enum (they were mislabeled 'Servo-only' and the script body param was documented as `script` when the handler reads `text`). Agents can now discover and correctly invoke pre-load injection.

Bundle: paginate endpoints/secrets lists

The bundle `endpoints` and `secrets` tools returned every row in one JSON response, which the MCP response cap truncated mid-record for large sessions (500KB secrets / 100K+ endpoints). Both now accept `limit`/`offset` and return `{items, total, has_more}` so a caller can page through the full set.

Scope: cross-project contamination regression guard

Added a regression test proving a host added to project A never resolves tagged with project B's id — locking in the live-scope-sync fix so the wildcard flakiness and cross-project scope leak cannot silently return.

v0.2.30

2026-08-10GitHub ↗
Scope: wildcard flakiness + cross-project contamination fixed

Project scope updates via the UI import path (`PUT /api/projects/{id}/scope`) now sync the live proxy scope when the project is active, so wildcard patterns no longer silently vanish from effective scope and in-scope hosts no longer resolve `in_scope:false`. This also removes the cross-project scope contamination where a stale live config picked up patterns from a previously active project.

HTTP/3: correct project attribution for overlapping scopes

HTTP/3 MITM flows now prefer the active project's scope before falling back to first-match, so with overlapping patterns (e.g. `*.caissedesdepots.fr` vs `ciclade.caissedesdepots.fr`) H3 flows land in the right project — matching the TCP proxy behaviour.

v0.2.29

2026-08-10GitHub ↗
CAPTCHA: self-contained hCaptcha + Cloudflare Turnstile solve

`captcha_solve` now handles hCaptcha Enterprise and Cloudflare Turnstile checkbox challenges — not just the geetest slider. Pass `captcha_type=hcaptcha` or `captcha_type=turnstile` with a `url` and Hugin launches a headed Chrome if needed, navigates to the page, finds the captcha's cross-origin iframe, and clicks the checkbox to extract the token. Fully self-contained: no pre-existing browser session and no third-party solver key required for the click path.

Browser: cross-origin iframe automation primitive

The Chrome/CDP backend gained `list_frames` and `execute_js_in_frame` — CDP `Runtime.evaluate` with an explicit execution-context id — reaching into cross-origin iframes (hCaptcha, reCAPTCHA, Turnstile) that top-frame JavaScript cannot touch. This is the reusable primitive behind the new captcha solve and any future iframe automation.

CAPTCHA: optional solver-API bridge

When a checkbox click escalates to an image challenge, `captcha_solve` can fall back to a 2Captcha or CapSolver task (`HCaptchaTaskProxyless` / `TurnstileTaskProxyless`) via the new `captcha_solver` config section (`provider` + `api_key`, key redacted on read). If no solver is configured it reports a clear not-solved reason — the bridge is never required.

Synaps modules: live WASM feed

The community scanner-module feed is now live — the 17 sandboxed WASM modules install from the official release, and every catalog path (`update sync`, `update install <id>`, MCP install, REST) defaults to the feed so no `--base-url` is needed. Download is flat `{base}/{module}.wasm` with sha256 verification.

Intruder: every vulnerability-class payload corpus wired

All 30 advertised vulnerability classes now expose a full payload corpus as a builtin Intruder wordlist and to the agentic payload generator — the 24 classes that previously fell back to a single placeholder (`test_payload_1`) now emit real corpora (XXE, HTTP smuggling, GraphQL, JWT, deserialization, CORS, host-header, OAuth, HPP, cache deception, EL/XML/email injection, prototype pollution, and more), grounded in the corresponding scanner checks.

v0.2.28

2026-08-09GitHub ↗
Open redirect: semantic detection — DOM taint, header-driven, chain-following

Open-redirect detection no longer fires only when the literal canary `evil.com` comes back. The passive check now catches DOM-taint redirects (a user-controlled source — URL param, fragment, postMessage, referrer, window.name, localStorage — feeding a navigation sink like `location.href`, `location.assign`, `window.open`, `srcdoc`, `$.ajax`, `XMLHttpRequest.open` via a *variable*), header-driven redirects (`Host` / `X-Forwarded-Host` / `X-Forwarded-Proto` / `Referer` reflected into `Location`), and flags relative/same-host `Location` as redirect-chain candidates for the active check to follow.

Open redirect: canary-agnostic + OAuth redirect_uri validation

The active check now URL-decodes + HTML-entity-decodes + strips whitespace/control before matching, so an encoded or obfuscated reflection of the canary (`%65vil.com`, `evil%2ecom`, `e&#118;il.com`, null-byte truncation) is still caught. OAuth/SAML `redirect_uri` params get semantic validation — host-suffix, userinfo (`@`), backslash, and cross-host redirects on OAuth-shaped params are flagged as the highest-impact class (they leak the authorization code).

Open redirect: full bypass payload corpus + body/header params

The active check now routes through a 127-entry bypass corpus covering every 2024–2026 technique family (double-encoding, trailing-slash+?, host-suffix, userinfo, backslash, Unicode/IDN homographs, hex/octal JS escapes, CRLF Location injection, RTL-override, null-byte, javascript:/data:/vbscript:/file: schemes). The MCP `open_redirect scan_active` now also probes body/JSON params (mass-assignment) and header params, and follows redirect chains up to 5 hops to the final target.

v0.2.27

2026-08-09GitHub ↗
Session profiles: create/update no longer drop the auth header

`session_profiles create` and `update` silently discarded a supplied `header_name`/`header_value` — the profile came back with `headers: []` and was useless for BAC/authz-matrix replay until a separate `add_header`. Both actions now persist the initial header, so a profile created with `Authorization: Bearer <token>` is immediately replayable.

BAC audit: no more silent no-op on scope-blocked flows

`bac_audit` returned `flows_analysed:0 / requests_sent:0 / findings:0` with no error when every candidate flow was rejected by the scope gate, and the engine's replay guard used the process-global scope while the flow gate used the session-aware scope — for a session-bound peer these disagreed, so flows that passed the gate were re-filtered out by the engine. The engine now uses the same effective scope as the gate, and a zero-flow audit fails loudly with a clear skip-reason instead of silently doing nothing.

HTTP/3 request: real HTTP/2 fallback

`http3 request`'s `fallback_h2` flag was config-only — a target that doesn't speak QUIC always failed with 'QUIC handshake failed / timed out'. On QUIC failure it now retries over TCP via ALPN (HTTP/1.1 or HTTP/2), returning `protocol: h2-fallback` with the real response.

Crawler: probe_methods now probes write verbs

`crawler probe_methods` only sent OPTIONS. It now cycles OPTIONS + POST + PUT + PATCH + DELETE (bodyless, non-destructive) to discover which mutation verbs a route accepts — surfacing mass-assignment / verb-tampering surfaces the crawler previously missed.

Unified auth context: session profiles attach to vurl_http and repeater

Session profiles were only attachable to the crawler via `custom_headers`. A new `profile_id` param on `vurl_http` and `repeater send` resolves a profile's auth headers (by id or unique name) so every tool carries the same auth context as BAC replays instead of hand-injecting `Authorization`/`Cookie` on every call.

v0.2.26

2026-08-08GitHub ↗
License: proof-of-work required to start a new trial

Trial farming (a patched client + IP rotation mints a fresh 7-day Pro trial, forever) survives the device-based / no-account / no-telemetry model because the per-IP and per-device caps key on the same rotatable client inputs. Minting a NEW trial now requires solving a hashcash challenge: a nonce such that SHA256(fingerprint ‖ nonce) has 20 leading zero bits. The proof is bound to the fingerprint, verification is pure public hashcash (no server secret, no identity, nothing new learned about the user — zero telemetry), and existing active trials re-check for free, so a legit user pays a fraction of a second once per 7-day window while a farmer pays it per mint.

License: trial tokens are now device-bound in the signature

Trial tokens are minted bound to the requesting device fingerprint; the client refuses a token whose signed fingerprint claim doesn't match this device. This moves the cross-machine trial-token replay guard from a client-side-only check into the signature authority itself, closing replay of a token minted for device A on device B.

Installer: large downloads no longer time out

The `hugincyber` crates.io installer's HTTP client used reqwest's 30-second default as a total-request timeout, which covers reading the entire response body. A 100+ MB release tarball (the macOS CLI asset is ~111 MB) reliably exceeds 30 s on a real-world link, so `cargo install hugincyber` aborted with "operation timed out" and no one could install. The installer now uses a dedicated client with a 20-minute total-request timeout, matching the self-updater's fix.

v0.2.25

2026-08-07GitHub ↗
Updater: large downloads no longer time out

The self-updater's HTTP client used the 30-second default as a total-request timeout, which covers reading the entire response body. A 100+ MB release tarball (the macOS CLI asset is ~111 MB) reliably exceeds 30 s on a real-world link, so every `hugin update` aborted with "operation timed out: error decoding response body" and no one could update. The large asset download now uses a dedicated client with a 20-minute total-request timeout; the small metadata/checksum/signature requests keep the fast 30 s default.

v0.2.24

2026-08-07GitHub ↗
License: hardware-anchored trial + per-IP distinct-fingerprint ceiling

A free trial is now only issued when the device identity is hardware-anchored (hw-<sha256> from stable DMI/machine-id/serial signals). A machine with no stable hardware signal resolves to a generated UUID, which an attacker could reset by deleting ~/.hugin/device.id and reconnecting — a free-trial farm. The trial path now refuses such devices, so there is nothing to reset. Server-side, a single IP that starts trials for many distinct fingerprints (evading the per-device cap of 1) is now bounded by a per-IP DISTINCT-fingerprint ceiling (default 3, configurable).

License: strict server-token Pro + persisted clock floor

Offline Pro now requires a server-issued token. The forgeable local trial record (static HMAC key + plaintext device.id, both recoverable from a shipped binary) can no longer grant Pro when the license server is unreachable — an attacker could previously rewrite trial.record started_at for unlimited full-Pro offline. Offline Pro is now bounded by the 24h server-token window, and the clock floor is persisted so a rolled-back system clock can't extend it.

Repeater: effective scope is now session-aware

A session-isolated MCP peer bound to project B was enforced against the process-global scope snapshot (project A), so repeater raw_send/send rejected in-scope wildcard-covered hosts that vurl_http and the browser accepted. Repeater now resolves the effective scope through the same session-aware active_scope_config() seam, so raw_send/send agree with vurl_http.

Oastify: native start surfaces loopback/private + DNS-mismatch warnings

The REST oastify_native_start (which the UI drives) returned only {"success":true} — no warning when external_ip is loopback/private/unspecified or when domain=oastify.eu resolves to the remote server. An operator starting the native server with a loopback external_ip got a silently-dead OOB surface: listeners bind, poll returns 0, and every blind SSRF / OOB callback test is a guaranteed false negative. The start response now returns external_ip, external_ip_warning, and dns_mismatch_warning (parity with the MCP path), and the UI renders the warning banner.

Google fuzzer: authenticated reflected-XSS scanner

google_xss_scan now runs authenticated reflected-XSS probes against Google web apps that the API-key probe layer can't reach (they 401/ServiceLogin-redirect without FPA auth). It classifies each query param's reflection context (script / single|double-quoted attr / unquoted attr / url attr / raw text / html_escaped) and fires context-appropriate payloads.

v0.2.23

2026-08-04GitHub ↗
Installer: Windows x86_64 is now supported

`cargo install hugincyber` on Windows now picks the prebuilt hugin-cli-windows-x86_64.exe and installs `hugin.exe` — the installer matches (platform, arch) to an asset (`windows` + `x86_64`), falls back to `USERPROFILE` when there's no install dir set, splits the PATH check on `;`, and prints a PowerShell hint when the install dir isn't on PATH. The platform table in the README is updated to match (Windows x86_64 ✅; Linux aarch64 remains not shipped).

install.sh: valid Info.plist in the macOS .app bundle

The curl installer's app-bundle template wrote `<string>APPL</key>` — a mismatched close tag that produced an unparseable Info.plist, so LaunchServices refused to launch the freshly installed app. The tag is now closed correctly and the plist version fields render numeric (the GitHub tag's leading `v` is stripped). Regression-tested: the install script's plist heredoc must parse as well-formed XML.

v0.2.22

2026-08-04GitHub ↗
Scope enforcement is now session-aware end to end

A session-isolated MCP peer bound to project B was enforced against the process-global scope snapshot (project A): `project.activate` bound the peer to B while the browser, crawler, vurl, and — worst — the entire active-scan tool family (idor, authz, xss, sqli, pathtraversal, pipeline, bac_audit, open_redirect, session_profiles) still judged every in-scope B flow as out of scope (`different_project=0, out_of_scope=N`). Active scans against a freshly-activated project silently rejected every candidate flow. All gates now resolve the effective scope through the single session-aware `active_scope_config()` seam — a peer's bound project's scope, falling back to the global live config (never the permissive capture-all default) — so `scope test`, `list_flows in_scope`, and every active scan agree on what is in scope.

Crawler: real-browser fallback when Cloudflare/DataDome blocks the static engine

When the static HTTP crawler hits 5 consecutive WAF/challenge blocks (403/426/503 with an anti-bot body), it now lazily spins up a headless Chromium through the same proxy (with `--ignore-certificate-errors` so the Hugin MITM CA is trusted), re-queues the blocked URLs, and replaces the challenge response with the rendered DOM (status 200, real final URL). Cloudflare JS challenges that no HTTP/TLS fingerprint stack can pass are now crawled for real, with the page's links/forms/scripts extracted. The re-queue bypasses the frontier's seen-set dedup via `UrlFrontier::push_requeue`, which previously silently dropped the blocked URLs and made the fallback crawl nothing.

bac_audit: resolves the selected flows instead of skipping the corpus

The BAC engine's flow gate read the global scope, so an audit against a session-bound project analyzed 0 flows and reported `requests_skipped=5000` (the whole corpus). With the session-aware gate, `bac_audit flow_ids=[...]` now actually analyzes the given flows.

v0.2.21

2026-08-03GitHub ↗
Updater: release numbering fixed so existing installs can actually update

The previous hotfix was cut as v0.2.2 — which sorts numerically LOWER than v0.2.19, so `is_newer(latest=0.2.2, current=0.2.19)` returned false and the built-in updater declared every 0.2.19–0.2.2 install "already up to date." This release is v0.2.21, which compares higher than every prior version, so `hugin update` offers it to all existing installs.

Updater tests: can no longer clobber a real install

A regression test in the updater suite wrote a fake 2 MiB binary and called the real `sync_install_locations`, which copies the running exe over every install location (/usr/local/bin/hugin, ~/.local/bin/hugin, the .app bundle) — on a dev box with Hugin installed, `cargo test` replaced real binaries with the fixture and patched the .app Info.plist version. The production entry is no longer called from tests (the sibling list is asserted via the injectable constructor), and the size plausibility guard was raised to 16 MiB — far above any test fixture, far below any real build (~270 MB release) — so a corrupt, truncated, or fake binary can never propagate to a real install again.

Updater: Info.plist corruption fix + self-heal

Earlier builds patched the .app bundle Info.plist with raw string surgery and could leave the XML malformed, after which LaunchServices refuses to launch the app. The updater no longer hand-edits the plist: it validates with `plutil -lint` and either bumps version strings through Apple's own plutil (canonical XML) or rewrites the plist from a fixed template. Runs at every startup and after every update, so a broken bundle repairs itself.

v0.2.2

2026-08-02GitHub ↗
Updater: fix regression where running the test suite destroyed installed binaries

The updater's own unit tests wrote a fake 6-byte `b"BINARY"` fixture and called the real `sync_install_locations`, which copies the running exe over every install location (/usr/local/bin/hugin, ~/.local/bin/hugin, the .app bundle). On any machine with Hugin installed, `cargo test` replaced the real binary with that 6-byte fixture — the installed `hugin` became the literal text "BINARY" and `hugin update` stopped working. The sync core now takes an injectable sibling list so tests only ever touch temp dirs, and it refuses to sync a current exe smaller than 1 MiB — a corrupt, truncated, or fake binary can never propagate to a real install again.

Updater: Info.plist corruption fix + self-heal

Earlier builds patched the .app bundle Info.plist with raw string surgery and could leave the XML malformed, after which LaunchServices refuses to launch the app ("incorrect executable format"). The updater no longer hand-edits the plist: it validates with `plutil -lint` and either bumps version strings through Apple's own plutil (canonical XML) or rewrites the plist from a fixed template. Runs at every startup and after every update, so a broken bundle repairs itself — no manual fix needed. Tests only ever exercise temp bundles, never /Applications.

v0.2.18

2026-08-02GitHub ↗
MCP: plugin host_call_tool deadlock fix

Plugin tools routing through Hugin's intruder/scanner/BAC/repeater engines (google_fuzz_endpoint and any plugin using host_api::call_tool) hung indefinitely — the MCP response was never returned. The host tool dispatcher used block_in_place + block_on inside a spawn_blocking thread, pinning the thread while the async tool work needed tokio worker threads to progress. Now spawns the async future on the runtime and waits via a channel, so the blocking thread yields while the runtime processes the work.

Proxy: multi-agent capture + OOS policy via project scopes

When session isolation is on, the global scope_config belongs to one agent's project — other agents' traffic was silently dropped. The proxy now captures flows matching ANY project's scope (not just the global), uses the matching project's OOS tag/hide/skip_persist policy (not the global's), and does not 451-block flows that match a project scope. Applied to both HTTP/1 and HTTP/3 paths.

Updater: surfaces stale-sibling sync failures

hugin update silently swallowed permission-denied errors on /usr/local/bin, leaving stale binaries forever with no indication. Sync failures are now tracked and printed with the reason and the fix (sudo hugin update or re-run the install script).

v0.2.17

2026-08-02GitHub ↗
HTTP/3: RFC 9114 header compliance fix

Captured HTTP/1.1 flows replayed over HTTP/3 (Alt-Svc auto-promoted Direct sends) shipped `Host`, `Connection`, and other connection-specific headers verbatim — forbidden in HTTP/3 (RFC 9114 §4.2/§4.3), so strict front-ends like Google Front End rejected them with `400 malformed or illegal request`. The HTTP/3 client now scrubs hop-by-hop + `Host` headers on every outbound path (one-shot, streaming, pooled), matching the policy h3-race already used. Replaying an authenticated Google flow over h3 now returns the real response instead of a front-end 400.

MCP: self-healing stdio-UDS bridge

Hugin now regenerates the stdio bridge script (mcp-stdio-bridge.sh) on every daemon start, both GUI and headless CLI. External MCP clients (opencode, Claude Code) that spawn the bridge via posix_spawn no longer hit ENOENT when the script is missing or stale after an update. The script is written next to mcp.sock with the resolved socket path baked in, so non-default HUGIN_HOME layouts work without manual editing.

MCP: per-call timeout via _timeout_secs

Every MCP tool accepts a `_timeout_secs` parameter that overrides the default call timeout for that single invocation — long-running scans no longer force a global client-side timeout bump.

MCP: plugin FFI calls on spawn_blocking

Plugin tool FFI calls now run on a blocking thread instead of the async runtime, so a slow or hanging plugin library can no longer stall the MCP event loop.

CI: CDN volume mount + verification fix

The release pipeline now correctly mounts the CDN staging directory into the runner container (was silently ignored by the runner config valid_volumes list), and the public CDN verification step hits nginx directly instead of going through Cloudflare (which returned 403 to curl). Release artifacts now reliably land on hugin.nu/releases/<tag>/.

CI: CDN verification via file hash + build-job version-check

The release pipeline verifies staged tarballs by SHA256 file hash against the public hugin.nu URL (not an HTTP status), and build jobs run the version+changelog gate before compiling — a mismatched tag fails fast instead of building for 90 minutes first.

Google plugins: concurrency + defaults

google-recon discovery_scan and zhandler_scan now probe concurrently instead of serially, and the default spatula count dropped from 20 to 2 to keep sandbox scans quick.

BAC matrix: denial findings require a real differential

The matrix classifier flagged every shadow 401/403/404 as an "access denied" finding even when the baseline returned the exact same status — a sweep against 10 targets filed 13 identical-status false positives out of 14. Redacted findings now fire only when the shadow's denial status differs from the baseline (e.g. baseline 401 → shadow 403 = identity recognized), and only shadow 401s / login-redirects count toward the stale-auth auto-disable counter (a 403/404 proves the identity works, it no longer disables a healthy identity after two noise hits).

v0.2.16

2026-07-31GitHub ↗
Installer: downloads now route through the hugin.nu CDN

`cargo install hugincyber` previously pulled the ~110MB binary from GitHub's rate-limited CDN. The installer now rewrites every release-asset URL to `hugin.nu/releases/<tag>/<asset>` (nginx static — fast, no rate limit), matching the in-app updater. SHA256 sidecars are still verified after download, so this is not a trust boundary.

Installer: Forgejo fallback URL fixed

The installer's fallback used `http://hugin.nu:3000`, whose port 3000 is not publicly exposed — the fallback always timed out. It now uses `https://hugin.nu/api/v1/...` (nginx-proxied), matching the updater.

Install docs corrected to match published builds

Removed download commands for never-published artifacts (linux-aarch64 CLI and .deb, AppImage, Windows desktop .zip, bare macOS .dmg). macOS desktop now points at the real `.app.tar.gz` bundles, the Windows section matches the shipped CLI `.exe`, the verify-download checksum filename is fixed, and the AUR package name is corrected to `hugin-bin`.

Release pipeline: CDN staging hardened

The release job now bind-mounts the CDN staging directory into the container and verifies every staged tarball through the public `hugin.nu/releases` URL (sha256 match + `latest` symlink serves the new tag) before reporting success — a release can no longer log success while the CDN never received the files.

v0.2.15

2026-07-31GitHub ↗
MCP tools: clearer action & parameter docs

The OOB tools (oastify / vurl_oastify) now document the full action list and clarify that there is no bare `register` action — use `register_batch` / `register_remote`. The `start` action's required `external_ip` and the `https=true` requirement for `tls_cert_path` / `tls_key_path` are now stated in the schema, not just the runtime error.

OOB connect: explains credential-less connects

Connecting without an api_token or admin_key now returns a plain-language note explaining that only the local tracker was initialised and that remote-dependent actions (poll, interactions, health_check) will fail until you reconnect with credentials — instead of a silent `connected:false`.

Discover: results_get returns a 'not ready' status

Calling `results_get` on a still-running discover job no longer fails with a filesystem error. It now returns a structured `ready:false` status with live progress counters and a hint to poll `status` until the job completes.

Discover: sandbox timeout guidance

The `scan_timeout_s` parameter docs now note that sandbox / high-latency hosts (~500-900ms per request) need a higher timeout — a 292-word list can exceed the 120s default on a sandbox. Raise to 600+ or reduce the wordlist.

Param discover: explains 0-confirmed results

When parameter discovery tests many params but confirms none, the output now adds a note explaining the target likely uses a fixed schema (protobuf) that ignores unknown params, rather than leaving `confirmed:0` with no context.

CORS scan: distinguishes method-not-allowed from no CORS

CORS scans against endpoints that reject the probe method (405 / 404 — common for POST-only / protobuf endpoints) are now classified as `method_not_allowed` instead of `no_cors`, with a note to re-scan a flow using the endpoint's real method via `scan_flow`.

Scanner: skips SQLi on protobuf / gRPC bodies

The SQL-injection check now skips raw-body insertion points on opaque binary content types (protobuf, gRPC, cbor, msgpack, octet-stream). Injecting SQL into a binary blob corrupts the framing — the differential is a parse error, not SQL evaluation — so these were always false positives.

Scanner: SQLi UNION canary reflection guard hardened

The UNION/stacked-query canary reflection FP guard now normalises HTML entities (`&#39;`, `&quot;`) and URL encodings (`%27`, `%20`) before comparing, so a 404 error page that echoes the request URL with escaped quotes no longer passes as a confirmed SQL injection.

Plugins: reload hot-swaps tool routes

`plugins.reload` now re-wires the live MCP tool router with the freshly-loaded plugin routes and drops stale library handles, so repairing a broken plugin (fix the file, sign, reload) makes its tools callable without a full Hugin restart. Previously reload only refreshed the info list while the dispatcher kept routing through the stale boot-time routes.

Plugins: new `sign` action

`plugins.sign path=<dylib>` signs a plugin binary with the operator's collab Ed25519 key and writes the sibling `.sig` file the loader verifies at dlopen time. The rebuild→sign→reload workflow is now fully self-contained in Hugin — no external Python or signing script needed.

Plugins: typed MCP schemas for recon tools

The recon plugin's six tools (project_filter, sandbox_variants, spatula_forge, req2proto, proto_schema_leak, bearer_token) now expose their real input parameters in the MCP tool schema instead of an empty `properties:{}`. The agent sees what each tool accepts and stops getting 'X required' errors from schemaless calls.

Plugins: completeness check no longer reports empty catalog as complete

`google_confirm_complete` now returns `complete:false` with a 'catalog is empty' note when the discovery catalog has no endpoints, instead of `complete:true, missing_count:0` which let the agent finish without testing anything.

Repeater: raw_send no longer double-prepends https://

Passing `target` as a full URL (`https://host`) to `repeater raw_send` no longer reaches the DNS resolver as `https://https://host` ('invalid dns name'). `parse_target` now strips a leading scheme so the raw-socket dial and the scope check agree on the host.

Pipeline: timeout guidance for full battery runs

The `pipeline` tool's `timeout_s` docs now note that a full 7-stage run (flow_analysis → authz → idor → sqli → pathtraversal → xss → bac) over 6+ flows routinely exceeds the 300s default, and recommend `timeout_s=600`+ for full battery runs since each stage sends multiple probes per flow.

v0.2.14

2026-07-31GitHub ↗
Scanner: reduced false positives on hosted platforms

Several active and passive checks now recognize platform-standard behaviors on hosted database and API services — path-normalization redirects, wildcard CORS, and URL-based API key authentication are no longer flagged as vulnerabilities when they match documented platform conventions.

Scanner: smarter error-response handling

Active checks that probe for schema introspection and credential validation now distinguish genuine success responses from error responses that merely echo the request payload, and check API status fields rather than relying on HTTP status codes alone.

CLI: auto-restart after update

hugin update now relaunches the app automatically after a successful self-update — no manual restart needed.

v0.2.13

2026-07-30GitHub ↗
Updater: downloads from hugin.nu CDN

hugin update now downloads release binaries from hugin.nu (nginx static, no rate limit, no WAF) instead of GitHub's CDN which timed out on 110MB+ downloads. SHA256 and Ed25519 signature still verified after download. GitHub API remains the metadata source; only the file download is redirected.

Release pipeline: auto-stage to CDN

The release pipeline now automatically stages all release assets to /opt/hugin-web/static/releases/<tag>/ and updates the 'latest' symlink, eliminating manual asset copying.

Release pipeline: Windows race fix

Windows build added to release job's needs with continue-on-error, so release waits for Windows but ships regardless of outcome. CARGO_BUILD_JOBS bumped to 4 for faster Windows compilation.

v0.2.12

2026-07-30GitHub ↗
macOS: no more keychain password prompts

Hugin now detects adhoc-signed binaries at runtime and falls back to file-based secret storage (~/.hugin/ with 0600 permissions). Every update produced a new adhoc signature that broke the Keychain ACL, causing a password prompt on every launch. File fallback is equivalent security for single-user workstations. When a notarized Developer ID signing step is added to CI, it automatically switches back to the keychain.

Install script: downloads from hugin.nu

curl|sh now downloads from hugin.nu (same server, fast) instead of GitHub's CDN which can be slow or throttled from some networks. GitHub remains as fallback. Progress bar fixed, 3 retries with backoff, release assets served directly via nginx.

Release pipeline: draft before publish

Releases now create as draft → upload all assets → verify required tarballs present → undraft. Eliminates the race window where 'latest' flips to a new tag before 100MB+ assets finish uploading, causing 'no build found' for users who install during the upload.

Windows cross-compile: boring-sys fix

Fixed fiat_p256_adx_mul/sqr link failure by setting OPENSSL_NO_ASM in the mingw toolchain file. Windows is not mandatory — ships as bonus artifact when it succeeds.

v0.2.11

2026-07-30GitHub ↗
Editor: fixed text selection double-layer

The transparent textarea overlay no longer leaks its own glyphs over the highlight backdrop, and the backdrop no longer steals pointer events in readonly mode. Selection now works correctly in both editable and read-only request/response panes.

Cmd+F finder: fixed wrong-row highlight

The row-level find highlight no longer lands on an unrelated table row when the match is in an editor or response pane. The CSS Custom Highlight is now the sole highlight for editor content; the row background only applies to real row containers.

Install script: fixed download failure

The curl|sh installer now resolves the asset URL via the GitHub API instead of the redirect chain that could fail silently. SHA256 verification now correctly checksums the archive, not the extracted binary.

v0.2.10

2026-07-29GitHub ↗
Browser: CDP alias and Chrome default

browser_type="cdp" now routes to Chrome instead of silently launching Mullvad. Taint, antibot, session capture, and cookie tools now default to Chrome so CDP-dependent features (DOM Invader live probes, taint instrumentation) work out of the box.

Update button: always visible + restart

The status-bar update badge is now always visible — shows "Up to date", "vX available", "Updating...", or "Restart" after a successful update. Clicking Restart re-execs the binary immediately. Settings > Account "Check Now" also lights up the badge.

Updater: asset-readiness gate

Updater::check() returns "up to date" when the latest release has zero assets, preventing a broken download when a tag is pushed but CI hasn't finished uploading release artifacts.

v0.2.9

2026-07-29GitHub ↗
Intercept: response stall in observe mode

When Intercept ON + Responses ON + Hold OFF, every response blocked on a oneshot channel waiting for a user decision — the browser stalled with ERR_TIMED_OUT. queue_response now gates on hold mode, matching the request-side fix from v0.2.4.

Endpointer: project-scope isolation

feed_endpoints now uses the project's ScopeConfig (loaded from the store) when project_id is supplied, instead of only the workspace-level scope snapshot. Endpoints from other projects' scope no longer pass through.

Endpointer: proxy-aware HTTP client

feed_endpoints now routes probes through the Hugin proxy (127.0.0.1:{proxy_port}) when the proxy is running, so the endpointer benefits from the same network path (VPN, DNS, upstream proxy) as browser traffic. Fixes no_auth_status: 0 on all endpoints.

Endpointer: probe failure reporting

Network errors (DNS failure, timeout, connection refused) are now logged at WARN and exposed as probe_error per-endpoint + probe_errors count in the feed report — no longer silently swallowed as status 0.

Updater: DNS false-positive fix

DNS consistency check now compares IP sets (overlap) instead of single IPs — DNS round-robin on CDN-backed domains (GitHub) no longer triggers false 'DNS MISMATCH' warnings on every update.

Updater: permission-denied noise fix

Sibling sync now logs DEBUG instead of WARN for permission-denied on /usr/local/bin/hugin — expected when not root, not a warning-worthy event.

Ctrl+Space intercept toggle fix

The keyboard shortcut and command palette now call the backend API when toggling intercept, not just the local UI signal — the proxy learns about the toggle immediately.

BAC scanner: false-positive reduction

Mass-assignment echo-without-effect demoted to Tentative instead of firing Critical. Content-Type polyglot MergePatch variant removed — RFC 7396 is a standard content type, not a polyglot. Cross-identity echo on public endpoints suppressed when drift signal + no PII. HPP on generic non-resource responses suppressed via body_contains_resource_data guard. Every guard is conservative — no real findings lost.

v0.2.8

2026-07-29GitHub ↗
BAC: query-param auth carriers

key=, access_token=, id_token= and other query-param auth carriers are now recognized by the BAC engine — identity replay scrubs and swaps them just like Authorization headers. Critical for testing REST APIs that authenticate via URL params.

BAC: Firebase push ID classification

Firebase push IDs (-N... 20-char lexicographic keys) are now classified as a dedicated IdKind with predictable-pattern recognition, instead of falling through to the generic Slug bucket.

BAC: identity-injection bypass headers

X-User-Id, X-UID, X-Account-Id, X-Customer-Id, X-Tenant-Id, X-Org-Id, W-User-Id added to the bypass header corpus — a confirmed paid ATO class where debug headers left from test environments were honored in production.

BAC: HPP (HTTP parameter pollution)

param_mutation now appends a duplicate query param with a foreign value (?id=SELF&id=FOREIGN). Server-specific parsing (Apache/PHP first, Express array, .NET comma-joined) means the auth middleware may read the first copy while the handler reads the last.

BAC: me/self path segment IDOR

/api/users/me, /api/users/self, /api/users/current path segments are now treated as ID candidates — swapping the self-alias for an explicit foreign ID is the classic me-to-IDOR bypass.

BAC: GraphQL Relay node() bypass

When the schema exposes a Node interface, the scanner probes node(id:...) with Relay global IDs mined from captured flow bodies. node() is a universal object resolver that often bypasses field-level authz on type-specific queries.

BAC: ID value mutation table

Numeric ID params are now tested with 6 parser-quirk variants: negative, zero, leading zeros, float coercion, wildcard, and null-byte truncation — targeting server-side type coercion bugs.

BAC: Firebase Auth field recognition

Firebase Auth response fields (localId, custom_token, refresh_token, sign_in_provider) and x-firebase-appcheck header added to the role-field and auth-carrier recognition lists.

v0.2.7

2026-07-29GitHub ↗
Endpointer feed_endpoints: concurrent probing

feed_endpoints now probes endpoints in parallel (default 20, configurable via concurrency param, clamped 1-100) instead of sequentially — large batches of endpoints are fed into the scanner in a fraction of the time.

Endpointer feed_endpoints: scope pre-filter

Out-of-scope URLs are pre-filtered before any network request is issued, so large batches don't waste probes on third-party CDNs/analytics hosts that the scope gate would reject anyway.

v0.2.6

2026-07-28GitHub ↗
Full install-location sync

hugin update now syncs ALL known install locations — /Applications/Hugin.app, /usr/local/bin/hugin, ~/.local/bin/hugin — by SHA256 comparison, even when the binary is already up to date. No more stale copies where the Dock GUI reports an older version than the CLI.

DOM-XSS: storage→sink proximity FP guard

localStorage.getItem within 4096 chars of innerHTML no longer produces a HIGH-severity DOM-XSS finding without a proven taint chain. Same FP guard that existed for postMessage is now extended to Web Storage sources.

CSS color hash no longer flagged as hardcoded password

Password:"9200ee" (a Svelte CSS color token) no longer matches the hardcoded-password regex. 3/6-digit hex color values are now recognized as design tokens, not credentials.

PHP dd() regex no longer matches Svelte dd(e)

The bare dd( regex now requires a PHP sigil ($var) or quoted string argument, so Svelte's dd(e) function call is no longer flagged as a PHP debug dump.

ASP.NET error page regex tightened

CloudFront/nginx 403 error pages no longer match the ASP.NET yellow-screen-of-death detector — the regex now requires ASP.NET-specific markers (Runtime Error, __VIEWSTATE, System.Web) alongside the error title.

CDN error pages skip header checks

security-headers, clickjacking, permissions-policy, and referrer-policy checks now skip CDN-generated error pages (CloudFront, Cloudflare, nginx, etc.) so missing CSP/HSTS/X-Frame-Options on a 403 CDN page stops producing noise.

SPA wildcard redirect no longer flagged as admin/debug surface

admin_surface and debug_exposure now require 2xx — a 301 from a SvelteKit/Next.js catch-all router is downgraded to Info, not reported as a reachable admin panel. Eliminates 25+ findings per SPA scan.

Scanner SSRF probe no longer self-inflicted IP disclosure

169.254.169.254 (the metadata endpoint Hugin injects for SSRF testing) is no longer flagged as a private IP disclosure in request headers.

WebSocket message handlers not flagged as postMessage

ws.addEventListener('message') and socket.onmessage are now recognized as WebSocket handlers, not window.postMessage handlers — the postMessage check filters them out.

Localhost WebSocket not flagged as mixed content

ws://127.0.0.1 and ws://localhost (Web3 RPC, Vite dev server) are no longer flagged as mixed content on HTTPS pages.

Error responses skip cacheable-response check

4xx/5xx responses no longer produce 'sensitive response may be cached' findings — error pages carry no sensitive data.

JSON + trailing HTML no longer flagged as content-type mismatch

Response-splitting artifacts (valid JSON followed by a CDN error page) are recognized as transport artifacts, not content-type mismatches.

Bare C:/ in JS strings not flagged as internal path

Forward-slash Windows paths in minified JS string literals are skipped — real Windows path leaks use backslash separators.

v0.2.5

2026-07-28GitHub ↗
macOS .app sync on update

hugin update now syncs /Applications/Hugin.app in lockstep with the CLI binary and patches Info.plist version strings — no more two-binary split where the GUI reports an older version than the CLI.

Bounty platform session panel dismiss

Logout on YesWeHack, HackerOne, and Intigriti now collapses the session panel so it stops wasting vertical space after you've signed out.

v0.2.4

2026-07-28GitHub ↗
Rule-driven intercept hang fix

RuleAction::Intercept blocked proxy tasks forever with no timeout. Both request and response paths now use INTERCEPT_DECISION_TIMEOUT (5 min) so traffic flows even if the user doesn't act.

Observe mode respected by rules

queue_request now gates on hold mode — with Intercept ON + Hold OFF, Intercept rules no longer pause traffic. Matches evaluate_request behaviour.

macOS updater asset matching

Updater now accepts 'darwin' as an alias for 'macos' when matching release assets.

install.sh sha256 sidecar

Fixed sha256 checksum URL to use .tar.gz.sha256 instead of .sha256.

Oastify accept_invalid_certs

accept_invalid_certs now on the OastifyParams struct (was only on CrawlerParams — typed deserialization stripped it).

v0.2.3

2026-07-28GitHub ↗
Rules double-serialization fix

MCP-created rules now fire at runtime. action/conditions/scope_json were double-wrapped when MCP clients sent stringified JSON — fixed with a value_to_json_string() unwrapper.

Rules admin elevation

delete/config_set/migrate_ws now respect HUGIN_MCP_ADMIN_PEERS=* — switched from require_permission(None) to require_caller_permission().

Workflows trigger normalization

PascalCase triggers (FlowCreated) normalized to snake_case (flow_created) at create time, so dry_run/validate work.

Workflows enabled param

enabled parameter honored on create — defaults to false for programmatic safety.

Rules standalone validate

validate now accepts conditions + rule_action without an id, so you can validate before creating.

CI fix

crates.io publish now regenerates installer Cargo.lock to fix checksum drift.

v0.2.1

2026-07-27GitHub ↗
WAF fingerprint gaps

10 fixes: Chrome 138 UA, request jitter, TLS relay, sec-ch-ua, sec-fetch-*, Accept-Encoding, Accept-Language, H2 SETTINGS, Referer/Origin synthesis.

Async SQLi scan jobs

MCP scan_start/scan_status/scan_stop/scan_results — non-blocking bulk scans with job registry and concurrency cap.

Path parameter injection

list_params surfaces all insertion points including path segments. Error-based SQLi detection proven in path params.

Release signing

Ed25519 signing key fix — hugin verify now works against release artifacts.

macOS packaging

Ships .app.tar.gz instead of broken .dmg.tar.gz. Download page links fixed.

v0.2.0

2026-07-27GitHub ↗
MCP community gating

MCP tools gated to Pro license. Socket refuses connection when tier doesn't include Feature::Mcp.

Scanner limit param

max_payloads parameter on scan start to cap request volume.

BAC check order

WS IDOR + GraphQL alias auth checks added (18 total).

crates.io

hugincyber installer v0.2.0 published. cargo install hugincyber works.

v0.1.3

2026-07-18GitHub ↗
Platform fixes

macOS aarch64 + x86_64, Linux x86_64 builds. Ed25519 signed releases.

Scanner

55 active / 48 passive checks. BAC audit with IDOR, JWT escalation, mass assignment.

v0.1.1

2026-07-13GitHub ↗
Scanner FP cleanup

50+ false-positive and bug fixes from three live engagements (25 passive, 15 active, 11 BAC).

Proxy latency

pool_idle_timeout 10s + connect_timeout 10s. Kills 60-120s keep-alive inflation. Three startup deadlock fixes.

YesWeHack

Modal login with credential persistence.

Synaps

Import .yaml templates and .wasm modules from the UI.

Browser

no_proxy parameter works with browse and navigate actions.

Crawler

15s navigation timeout cap. Aborts after 5 consecutive WAF/challenge responses.

v0.1.0

2026-07-09GitHub ↗
First public release

Intercepting proxy, scanner (55 active / 48 passive checks), repeater, intruder, decoder, sequencer, RatRace, Oastify, MCP server, browser.

Single Rust binary

No JS runtime, no telemetry, no account required for Community tier.

Bug-bounty skills

Recon and hunt skills published as MCP resources.

Full release history on GitHub.