All notable changes to this project will be documented in this file.
The first-impression release. Foreground prox up now opens the TUI by
default and exits when the whole stack is dead (plan 026, plan 028); the
daemon grew a warning channel that puts mkcert's own untrusted-CA note and
unresolvable-hostname warnings in front of the user (plan 028, #97, #98) —
with CA detection kept off the critical path: the trust probe plan 028
introduced was removed again after measurement, in favor of generation-time
detection only (plan 029). Underneath: the integration suite was rebuilt for
isolation and truthfulness (plan 027), and the docs moved to Zensical with
the shared StrideLabs theme (plan 025).
-
A foreground
prox upwhose whole process stack has died now ends the session instead of sitting there supervising nothing (plan 028, #96). A typo incmd:used to leave the terminal open indefinitely, showing nothing, until the user thought to press Ctrl-C — the trigger channelprox upwaited on is closed by a signal,POST /shutdown, or a TUI quit, and by nothing a process does. It now notices for itself: once every process is dead and at least one of them ended incrashedorblocked(a partial crash with something still serving does not count, and an all-completedtask config or a deliberate API-driven stop does not either), the session tears down and prints the sameCrashed:/Blocked:summaryprox up -d,startandrestartalready print.This is a breaking exit-code change: a plain or piped foreground
prox uppreviously always exited0. It now exits non-zero whenever its whole stack ends up dead, printing the crash summary and a one-line hint before returning. A script that treated any foregroundprox upreturn as "the session ended, nothing more to check" now needs to look at the exit code the way it already should forprox up -d.The interactive TUI does the opposite on purpose. A TUI session does not auto-exit — the user is right there reading, and pulling the screen away would take the crash output with it. Instead it shows a persistent full-width banner between the process panel and the viewport border:
All processes have stopped — 2 crashed. Nothing is running. Press q to quit.Colour is emphasis only; the sentence survives ANSI stripping. This also applies inprox attach, since the banner is computed from the same process snapshots the TUI already streams.Not in the
-dchild.--detachshort-circuits straight to plain mode internally, so without an explicit guard this watcher would have run inside the detached daemon too — and killed it moments afterprox up -dtold the user "The daemon is still running; stop it withprox down", taking the API and the crash logs with it. The daemon staying up on a crashed stack is the existing, deliberate contract; only the daemon child is exempted. -
prox gained a channel for warning you about things it previously either swallowed or never checked (plan 028). Two long-deferred warnings (#97, #98) both needed a way to get from wherever prox actually observes a problem to wherever the person who typed the command is looking, across all three run modes — including the shared proxy daemon, whose own stdout/stderr are
/dev/null, and aprox up -dchild, whose stdout/stderr are.prox/prox.log. That channel now exists (domain.Warning{code, message, hint}), and it is additive on the wire in both directions: the daemon's register response andGET /statuseach gained awarningsfield.Warnings print as
Warning: <message>(with an indented<hint>line underneath, when there is one) in a plainprox up, in the TUI's pinned startup preamble (and therefore the system log andprox logs), and inprox status. A warning never changes any exit code — includingprox status's, which already has its own exit contract for crashed/ blocked processes and a down proxy — because turning a script red over an untrusted CA it never asked about would be its own kind of false alarm.API change (additive):
GET /statusgainswarnings(an array of{code, message, hint}, omitted when empty) andwarnings_sealed(a bool, always present). The latter matters because a warning can still be in flight when aprox up -dparent takes its one status snapshot after the readiness + settle wait; the parent now pollswarnings_sealed(bounded at 2.5s) rather than trusting a single race-prone fetch. SeeGET /status. -
proxnow surfaces mkcert's own untrusted-CA warning (plan 028, #97). Runningbrew install mkcert(or equivalent) without also runningmkcert -installused to leave prox reporting every process healthy while every HTTPS request through the proxy failed in the browser — mkcert itself explains exactly why, but in shared mode that explanation used to go straight to the daemon's/dev/null. prox now captures mkcert's output and carries its line verbatim through the warning channel above, followed by the hintrun 'mkcert -install' and restart prox. It still implements no trust-store detection of its own — that stays entirely mkcert's job, correctly, per OS/browser — and it withdraws the warning automatically once mkcert reports the CA as trusted again. Detection happens at generation time only: mkcert speaks when it generates a certificate, and that's the note prox carries. A machine whose certs already exist but whose CA later broke (OS reinstall, keychain reset) is not detected until something triggers a real generation — an accepted trade-off, since the probe that used to cover that gap cost ~217ms and two subprocesses inside the daemon's registration critical path (underlifecycleMu, delaying every other project's registration). The hint (run 'mkcert -install' and restart prox) still names the fix. -
proxnow warns when a registered hostname does not resolve (plan 028, #98).prox upprintsRegistered domains: app.sec.test, which looks fine right up until it's pasted into a browser and comes back NXDOMAIN, because a.testdomain needs local DNS setup that a public wildcard like*.lvh.medoes not. prox now resolves each registered hostname once at startup and warns, naming the actual configured domain and linking the local DNS guide, when one fails. The check only fires on a positive NXDOMAIN — a timeout, offline resolver, or any other lookup error is treated as "cannot tell", never as "unresolvable", so a developer on a plane or behind a network-sandboxed CI runner is not told their perfectly good setup is broken. Runs in both shared and standalone proxy modes and never delays startup past its own short budget. -
The TUI process panel states, and the requests pane's empty state, no longer rely on colour or silence to say what happened (plan 028, #92).
styles.Crashed/styles.Blockedandstyles.Stopped/styles.Completedhave always shared a style, so those pairs were only ever colour-distinguishable — nothing at all once ANSI is stripped (piped output,TERM=dumb, a screenshot) or to a colour-blind reader. Every process state butrunningnow appends a word to the name in the panel:web (crashed),migrate (done),api (blocked on: db),worker (waiting on: redis),db (stopped),web (starting),web (stopping).runningcosts nothing extra, so the steady-state panel is unchanged.The requests pane's empty state used to read "No requests yet — traffic through the proxy appears here" unconditionally, telling a project with no
proxy:block to wait for traffic that could never arrive. It now says why the pane is empty: "No proxy running — enable a proxy: block in prox.yaml to capture requests" when there is no proxy running this session, or "No requests yet — capture is off, so rows will show metadata only" when there is a proxy butproxy.capture.enabled: false. The request detail view gained the matching explanation for a record with no headers or body: "Capture is disabled (proxy.capture.enabled: false) — no headers or bodies were recorded". Also removed: the detail footer's[FOLLOW] N/M linestag, which described a scrolling state the detail view never actually has. -
prox up -d,prox startandprox restartnow report the resulting state, not just that the request was accepted (plan 027, #94). They exit non-zero when a process iscrashedorblockedwithin 500ms of starting, printing the same linesprox statusdoes, so the two commands can no longer disagree. Previouslyprox up -dexited 0 whileprox statusreportedcrasheda second later.This changes exit codes. A non-zero
prox up -dnow means "the daemon is up, its processes are not" — it names the offending process and points atprox down, since the daemon is still running and still needs stopping. Scripts that treated a zero exit as "everything is running" were previously being misled; scripts that treat a non-zero exit as "nothing started" now need to distinguish the two cases.The guarantee is deliberately modest: no terminal failure observed within 500ms, not verified state. A process that dies at 501ms still exits 0. Only
crashedandblockedcount —starting,stoppingandwaitingare transient, andcompletedis task success.startandrestarttake ~500ms longer on success, because proving the absence of a failure means waiting the window out. -
A process with no healthcheck configured now reports health
-instead ofunknown(plan 027, #100).unknownis reserved for a configured check that has not reported yet, so the two are finally distinguishable — including for a crashed process, which keepsunknownwhen it has a healthcheck.API change (additive):
healthonGET /processesandGET /processes/{name}gains the value"none", whichprox statusrenders as-.prox status --jsonpasses the raw value through, so a script matching"health":"unknown"to find un-healthchecked processes must now match"none".healthcheck.enabledalso stops being hardcodedtrueand reports whether the check loop is actually running. -
prox logs <unknown-process>now fails instead of printing nothing (plan 027, #95). It exits 1 and names the process, suggesting the closest match. Previously the name was silently used as a filter that matched no records, so a typo produced empty output and exit 0. A valid name that has logged nothing still prints nothing and exits 0 — silence is only an error when the name is wrong. Applies to the positional argument,--process, comma-separated lists, and--follow.
-
A crashed process no longer keeps its health checker running against a dead pid (plan 028, #107). A process that crashed on its own used to keep re-executing its
healthcheck:command — with whatever side effects that command has — until some laterStophappened to arrive, andhealthcheck.enabledonGET /processes/{name}reportedtruethe whole time (plan 027 made that field truthful, which is what made the bug visible). The health checker now stops the instant the process exits on its own, not only when it is explicitly stopped: a crashed (or otherwise terminally failed) process'shealthcheck.enabledreadsfalseimmediately, and its check command stops executing right then rather than continuing indefinitely until some later stop request happened to arrive. -
Errors no longer claim prox is down when it just answered (plan 027, #95). "Is prox running? Try 'prox up' first." was appended to every client error unconditionally, so
prox stop <not-running>printed the daemon's own reply followed by advice to start the daemon that had produced it. The hint now appears only when the daemon is positively unreachable.PROCESS_NOT_FOUNDalso names the process and either suggests the closest match or lists the valid ones, sourced from the running daemon rather than the local config file. -
prox up -dfailure diagnostics no longer stack up across runs (plan 027, #99)..prox/prox.logis append-only and was tailed by a fixed line count, so each failed attempt buried the current error further under previous ones — worst exactly while iterating on a brokenprox.yaml. Each daemon run now writes a marker identifying itself, and the tail is scoped to the current run. The log is still never truncated; history stays on disk. Reporting is bounded at both ends — only the end of the log is read, and a run that logged more than 200 lines is printed capped, and says so — so a huge log costs a huge print no more than it costs a huge read. A run marker that was cut short mid-write falls back to the unscoped tail rather than presenting the previous run's output as the current one's. -
A foreground
prox upnow opens the interactive TUI (plan 026). Previously it streamed plain logs and the TUI was reachable only behind an explicit--tui, or viaprox up -d+prox attach; the best view was the one nobody saw. This is a breaking UX change to foregroundprox up.The TUI is preferred, never required:
prox upfalls back to plain log streaming, silently and without an error, whenever the terminal cannot host a full-screen UI — stdin or stdout is not a terminal (a pipe, a redirect, CI, an agent harness),TERMis unset ordumb, or the process is not in the terminal's foreground process group (a backgroundedprox up &, which would otherwise stop onSIGTTINthe moment the TUI read the keyboard). Scripted and CI invocations therefore behave exactly as before. If the TUI itself fails to start, the session says so and degrades to plain streaming rather than failing a command that never asked for a TUI. That fallback replays what the log buffer still holds — the most recent 1000 entries, which on a quiet startup is everything and on a noisy one is not; anything older has already been evicted from the ring and is gone.Escape hatches:
prox up --no-tuifor one run, orPROX_TUI=0(alsofalse,no,off) for a whole shell.--tuiis retained as the explicit "require it" form — it now fails rather than falls back when the terminal cannot host a TUI, naming which condition failed — mirroring how--capturewas kept after capture became default-on. A flag that asserts something —--tui,--tui=false,--no-tui— beatsPROX_TUI, which is then not consulted at all;--no-tui=falseasserts nothing (it is the flag's own default spelled out loud) and falls through toPROX_TUIand the default.--detachshort-circuits the whole decision, soprox up -dis unaffected by any of it.qin theprox upTUI stops your processes, exactly as Ctrl-C does: the foregroundprox upis their supervisor, so nothing outlives it. This is unchanged behavior, but it is now the default view rather than an opt-in one, so the owner-mode footer readsq stop(attach still readsq quit) and the help modal namesprox up -d+prox attachas the way to keep processes running past quit. -
A
-dchild's stdliblogdiagnostics now reach.prox/prox.log(plan 026).daemon.SetupLoggingreassigns theos.Stderrvariable, but the stdlib logger captured the original at package init and the daemon child's real fd 2 is/dev/null— so everylog.Printfin a detached session was written to nowhere. Those lines (shared-proxy connection loss and recovery, API/SSE errors, net/http TLS handshake failures and handler panics) now land in the daemon log file, which is a real increase in that file's volume. -
Docs now use the shared StrideLabs theme (stridelabs-docs-theme v0.2.2), so this site and the other StrideLabs docs sites share one look without copying CSS between repos. The header pairs the StrideLabs owl with prox's own console icon; headings are Fraunces, body Inter, code JetBrains Mono. Page URLs, structure and heading anchors are unchanged.
zensical.tomlshrinks accordingly — the palette, fonts and feature toggles now come from the theme and were deleted here. The theme installs as a plain git dependency from a public repo, so there is no registry auth and forks can still build the docs.Fonts are self-hosted by the theme: the site no longer makes any request to
fonts.googleapis.comorfonts.gstatic.com, so the Google Fonts CDN never sees a visitor IP. (This covers fonts specifically — the repo integration still callsapi.github.comfor star and fork counts, sincerepo_urlis set.) -
Documentation now builds with Zensical instead of MkDocs + Material for MkDocs (plan 025). Material for MkDocs entered maintenance mode in November 2025; Zensical is the successor from the same team.
mkdocs.ymlis replaced by a nativezensical.toml, and the docs dependency group is a singlezensicalpin —mkdocs,mkdocs-materialandpymdown-extensionsare gone. Published URLs, page structure and heading anchors are unchanged, so existing links (including deep links) still resolve. The site adopts Zensical's "modern" theme variant, so it looks different;variant = "classic"restores the Material appearance. -
Both docs workflows now build
--strict. Previously only the PR build was strict andvalidation.linkswas warn-only, so a broken link could reach the published site. Zensical validates links and anchors natively and aborts under--strict.docs.ymlalso gaineduv.lockin its paths filter, which it was missing, so a lockfile-only docs bump now redeploys. -
pymdown-extensionsis no longer a direct dependency; it comes in transitively viazensical. The config still declares eightpymdownx.*extensions, so if a future Zensical release drops that dependency the docs build will need a direct pin again. -
Fixed the docs header lockup overlapping the site title in the mobile drawer. Behind the hamburger menu on a phone, the divider and project icon painted on top of the site name. Zensical sizes that drawer slot for a single glyph, so the wider lockup overflowed it. Fixed upstream in stridelabs-docs-theme v0.2.2; this bumps the pin. Desktop was never affected.
The TUI release. prox up --tui and prox attach are now one redesigned
TUI (plans 021–024, PR #102): a menu bar with View, Filter, and Theme
dropdowns; six truecolor theme presets plus user TOML themes; a per-view
filter query language with regex and status-range terms; styled log and
request rendering with level detection across common dev log formats;
grab-for-agent copy keys (request ID, curl, exact JSON); full mouse
routing with pointer-shape feedback; a centered help modal; and
persistent settings. Requests gained scroll-back paging to the full 5000
records, reconnect rebasing, and toggleable columns including request
IDs. Beyond the TUI: prox commands now refuse to control a different
project's instance (plan 020), and a crashing process's error is no
longer swallowed at startup. As always the shared daemon requires an
exact version match: restart the daemon and every project against the
new binary after upgrading.
- TUI redesign (plan 021): menu bar with View, Filter, and Theme dropdowns;
truecolor themes (six presets plus user TOML in
~/.prox/tui/themes/); filter query language on thesbar with a matching Filter menu; view toggles for process panel, timestamps, and soft-wrap; styled log/request/detail rendering; grab-for-agent copy keys (y/c/Y); and full mouse routing (wheel, row/chip clicks, double-click to open request detail). Settings persist in~/.prox/tui/config.toml. - Dropdown height clamping with scroll indicators (plan 022). Long menus (for example the Theme list on short terminals) window to the available frame height with “… N more …” indicators; the mouse wheel scrolls an open dropdown and is consumed so the viewport underneath does not move.
- Free-motion menu hover (plan 022). Hovering the menu bar slides an open menu across sibling cells; hovering dropdown rows moves the highlight (requires all-motion mouse reporting).
- Broader log-level detection for
level:filters and level tints. Beyond JSONlevel/lvland logfmtlevel=, the classifier now reads JSONseveritykeys (Cloud Logging / stridelabs-python deployed format), pino/bunyan numeric levels ({"level":30}),criticalas error, and a standalone UPPERCASE level token early in the line — covering python logging's dev format, tracing's text layer, pino-pretty, and uvicorn access lines, ANSI colors included. Previously these lines had no detected level, solevel:infofiltered them all out. - Requests scroll-back in the TUI (plan 018). Scrolling to the top of the
requests list now loads the next older page of requests from the ring
automatically (1000 at a time, up to the full 5000 the server retains), so
you can walk back through traffic that the initial sync did not carry. The
status bar reports the state:
loading older…while a page is in flight,start of historyonce the oldest retained request is on screen, and⚠ older: …if a page fetch fails — pressing up again retries. One page is ever in flight at a time, and paging is unaffected by an active filter or search: the filters apply to what you see, not to what is fetched. - A reconnect now rebases the requests list on the server's current window (plan 018). Requests carry no sequence numbers, so a client cannot prove that history it scrolled back to still joins up with a snapshot taken after a connection gap. Rather than showing a list with an invisible hole in it, a completed re-sync drops everything older than the snapshot it just fetched (and clears the list entirely when the server's ring is empty — a restarted or replaced daemon), then lets you re-page. This also fixes a wrong "stale?" marker: an in-flight request sitting deeper in the ring than the sync fetch reaches is no longer mistaken for one the server has lost.
- The processes panel now shows a health dot (plan 018). A process with a
healthcheck configured gets a small glyph after its name — a green
●while healthy, a red✗while unhealthy — styled independently of the process's own state color so it can't be swallowed or recolored. A process with no healthcheck (or one still reportingunknown) renders exactly as before. - Requests column toggles (plan 023). The View menu gains a Columns
section in Requests view: Time, Host, Method, Status, Duration, and ID are
individually toggleable (URL always on). Defaults are all on; choices persist
under
[requests]in~/.prox/tui/config.toml./search matches only visible columns; copy keys are unaffected. - OSC 22 pointer shapes (plan 023). On supporting terminals (kitty, WezTerm, Alacritty, Ghostty, …), the mouse pointer switches to a hand over menu-bar cells, activatable dropdown rows, and process chips.
re:regex filters for logs (plan 023). The logssbar acceptsre:<pattern>and-re:<pattern>terms (RE2, ≤256 bytes, compiled once at parse; multiple positivere:terms AND together).- Status-range filters for requests (plan 023). The requests
sbar now acceptsstatus:>=N,status:<=N, and inclusivestatus:N-Mranges (100–599) in addition to exact codes and4xx/5xxclasses.
?help is now a centered modal over the live TUI (plan 022). Logs and requests keep updating behind it; the wheel scrolls the modal when content is taller than the box; Esc,?,q, Enter, or a click outside the box closes it (replacing the old full-screen help view).- TUI unification (plan 018):
prox up --tuiandprox attachare now one TUI. The owner's TUI is the same API-client TUI attach has always run, so every feature, key binding, and fix lands in both at once. prox up --tuiruns the same API-client TUI asprox attach(plan 018). It is now a client of the API serverprox upstarts in its own process rather than a second, local-only implementation reading the supervisor and log manager directly. Everything attach mode gained — the reconnecting log/request/process streams, the stream-health status line, request detail and its live refresh, the cursor and filter behavior — is now what--tuishows, and the two can no longer drift. Quit semantics are unchanged:qstill stops the supervisor and takes the processes down with it (attach mode still leaves the daemon running), andPOST /shutdownplusSIGINT/SIGTERMstill quit the TUI and run the normal shutdown sequence and exit contract. The status line carries no "Connected via API" text under--tui: it is the owner's own TUI, not a remote attach.- Proxy request retention raised to 5000 records (was 1000; plan 018).
Every request ring — the standalone proxy, the shared daemon's per-project
rings, and each project's local forwarded ring — now keeps the newest 5000
requests, so you can scroll much further back through traffic before
records age out. The
?limit=ceiling onGET /api/v1/proxy/requests(and the daemon's equivalent) moves to 5000 with it: the ring size and the per-call limit are the same number by definition, because the shared-daemon forwarder has to be able to backfill a full ring in one fetch after a reconnect. - Captured bodies are now retained for the newest 1000 requests, not all
5000 (plan 018). Deeper retention would otherwise multiply memory by
five, since a captured body can be up to 64KB inline. On a ring that owns
capture (standalone, and the shared daemon's per-project ring), a request
falling outside the newest-1000 window has its request/response bodies
dropped — inline bytes released, spilled body files unlinked — while the
record itself and its captured headers are kept, along with body
metadata (size, content type, truncation). Opening such a request shows its
full metadata and headers with the body reported as
evicted, the same signal an over-budget spilled body already used. A slow in-flight request that ages out of the window before it completes keeps its completion status and headers, but its body is not stored. Each shared-mode project's local forwarded ring enforces this same 1000-record bound independently, ordered by request timestamp rather than ring position — the daemon's own eviction is silent, so without its own bound this ring would keep every live-forwarded body forever between registrations. It only ever drops inline bytes, since it owns no spilled files to unlink; a spilled body's file eviction is still the daemon's to report. - The TUI now syncs the newest 1000 requests rather than the whole ring (plan 018), so startup and reconnect cost stay flat as retention grows. Older records are fetched on demand as you scroll back (see scroll-back above); the TUI itself will hold up to the full 5000 once you have paged through them.
--tuion a non-interactive terminal is now an error (plan 018).prox up --tuiwith stdin or stdout redirected (a pipe, a CI runner, an agent harness) exits non-zero with--tui requires an interactive terminalbefore starting the supervisor, proxy, or API server, instead of starting everything and then drawing a full-screen TUI nobody can see or quit. Non-interactive callers want plainprox up(streams logs to stdout) orprox up -d.- Merged footer row (plan 023). Status text, view/follow/count badges, and key hints now share a single footer band with typed messages, precedence, and narrow-width hint degradation.
- Bordered viewport panel, dropdowns, and help modal (plan 023). The main content viewport has a rounded titled border; dropdown menus use rounded borders with a right-aligned hint column; the help modal uses a focused border colour and a title spliced into the top edge.
- Full-width selection band on FullFill themes (plan 023). The active cursor
row (including every wrapped display row of a log entry) paints a
full-viewport-width band;
/search hits inside the band keep their highlight colour. - Logfmt level detection tightened (plan 023).
level=/lvl=tokens must appear at line start or after whitespace so embedded tokens likexlevel=no longer match. - Settings persistence hardened (plan 023).
~/.prox/tui/config.tomlsaves use a flock-serialized read→merge→write→rename transaction with directory fsync so concurrent writers cannot clobber each other.
-
TUI menu bar, dropdown, and process-chip mouse clicks (plan 022). Hit rectangles were recorded during render but discarded because
Viewuses a value receiver — the live model never saw them, so menu and chip clicks were dead in the real app. Clicks now use a shared hit registry that survives the render copy. -
A prox command can no longer control a different project's prox (plan 020). Commands discovered a running prox from
.prox/prox.state, or failing that fromapi.portin the localprox.yaml, and then trusted whatever was listening there. With two projects on the sameapi.port— or one stale state file whose port had since been taken —prox statusreported another project's processes as yours andprox downstopped another project outright. Every client command now verifies that the prox answering owns this project before acting, and refuses by name when it does not:Error: prox is not running for this project. A prox for /Users/you/projects/other is listening on 127.0.0.1:5552. Run commands from that directory, or target it deliberately with --addr http://127.0.0.1:5552Identity is the project directory, compared by device+inode, so symlinked roots,
/tmpvs/private/tmp, a custom-c, andprox.ymlvsprox.yamlall still resolve to "yes, that's yours". Git worktrees of one repo are correctly treated as separate projects — they are separate directories running potentially different code — with no VCS knowledge in prox.--addrbypasses the check entirely for deliberate cross-directory work, and now genuinely does so forprox attach, which previously failed before it ever consulted the flag. -
The examples no longer teach a pinned
api.port(plan 020).api.portis dynamic by default and that is the safe mode, but four shipped examples presented a pinnedapi: {port: 5555}block as the baseline shape of aprox.yaml— so copied configs, including agent-authored ones, collided on one port. This is the root cause of the cross-project bug above. This repo's ownprox.yamlno longer pins the port either, so its dev API port is now dynamic; useprox statusto see the actual port. -
Foreground
prox upno longer swallows the reason a process crashed (plan 020). Logs were subscribed after the supervisor had already started the processes, so a process that died instantly — a typo incmd:, a missing binary — could emit its error before anything was listening, leaving the banner and then silence. The subscription is now established before any process starts. (Foregroundprox upstill does not exit on its own when every process is dead; that is tracked separately.) -
The version-skew message's instructions now work when followed (plan 020). On a shared-daemon version mismatch, prox told you to run
prox proxy stop --forceand thenprox upin each project — butprox upfails there ("already running"), bareprox restartrequires a process name, and the still-running old projects resurrected an old daemon within ~15s, so the retry hit the same error. The instructions are now phased — stop every project, confirm the proxy has exited, then restart them — which also removes the need for--forceand the resurrection race entirely. -
ctrl+cnow quits from every mode (plan 023), including the help modal and filter/search text entry, where it was previously swallowed. -
Help modal width on narrow terminals (plan 023). Side padding and then the border degrade before content so the box no longer overflows very small frames.
-
Filter query serialization (plan 023). Bare terms round-trip verbatim (never re-quoted); unknown
level:tokens are dropped on serialize. -
Frame-fill on light themes (plan 023). FullFill presets no longer leave default-background holes in chrome rows; footer error flashes use readable styling on light footer backgrounds.
-
JSON log rendering performance (plan 023). JSON object lines are parsed once at ingest (level detection and path=value summary share one unmarshal); wrap-on display keeps summary plus compact raw consistently.
-
Plain log/request text now carries the theme background (plan 024). Level-less lines rendered on the terminal-default background — 62% of the frame in the light theme. Every TUI-formatted line now renders through the base (or level-tinted) style, and the frame-fill scanner no longer blanket-exempts the log region: only cells inside a line's own child ANSI spans stay exempt.
-
Requests header/data column alignment (plan 024). The header rendered
Durationat 8 columns while data cells were 7 (ID and URL data sat one column left of their labels), and the Status header readSta. Header and rows now share one column-spec table;Statusis a full label with data padded to match. -
Footer band flush and fill (plan 024). The key-hint group is padded flush to the right edge instead of floating mid-band, and every footer segment — join spaces, stream-health
n/a, paging notices, truncation ellipses, and the filter text input itself — now renders on the footer background, eliminating default-background holes (including the 43-column hole while editing a filter). -
Border and glyph details (plan 024). Panel and help border titles render
─ Label ─(they rendered─ Logs────); a clamped dropdown's bottom border now stops one row above the panel's bottom border instead of landing on it; a hovered closed menu cell is now visually distinct from an open one; and Theme dropdown rows no longer repeat thethint. -
Wrapped-row hanging indent (plan 024). Wrapped log continuation rows keep the timestamp/process gutter instead of dedenting to column 1, and wrapped help descriptions hang-indent to the description column.
-
Selective log-filter allocation regression (plan 024). The plan-023
filteredEntriespreallocation reserved full capacity even with an active filter (50152 B/op); full preallocation now applies only when no filter is active, restoring selective filters to ~1000 B/op while keeping the no-filter fast path.
Hardening release. Capture is now visible-by-default (redaction removed — the ngrok/DevTools model; #80, plan 015), prox.yaml parsing is fully strict (a typo'd or unknown key anywhere is a precise load-time error; #83, plan 016), and the two known integration-test flakes plus the missing PR-time docs build are fixed (#82, plan 014). Both behavior changes are Breaking — read those entries before upgrading. As always the shared daemon requires an exact version match: restart the daemon and every project against the new binary after upgrading.
- Capture-time redaction is removed entirely (#80, PR #85, plan 015).
v0.2.3's on-by-default redaction replaced
Authorization/Cookie-class header values and sensitive query params with[REDACTED]/REDACTEDin capture records. It was a half-measure — bodies were always stored verbatim, so a token in a JSON payload landed on disk regardless — and it fought the point of a local inspector (ngrok's inspector, Chrome DevTools, Charles, Proxyman, and mitmproxy all show traffic verbatim). Captured URLs, headers, query params, and bodies (up tomax_body_size) are now stored verbatim, in cleartext, in in-memory records with >64KB bodies spilling to.prox/capture/~/.prox/capture(dirs0700, spill files0600— now pinned by tests). Theredact,redact_headers, andredact_query_paramskeys are gone; with this release's strict parsing a config still carrying them fails at load (proxy.capture: unknown field "redact") — delete the keys. Projects whose traffic must not be recorded should opt out entirely:proxy.capture.enabled: falseor--no-capture. - Unknown keys anywhere in prox.yaml are load errors (#83, PR #86, plan
016). The lenient re-marshal parse path silently dropped typo'd fields
under
processes:/services:(andhealthcheck:), unknown keys in theproxy:/proxy.capture:/api:/certs:blocks, and unknown top-level keys. Everything now parses strictly, matching thedependencies:/tasks:precedent, with every structural error batched into one sorted report:invalid configuration: processes.web: unknown field "stop_timout"; .... Duplicate keys are rejected too (literal ones by the YAML parser with line numbers; alias-key duplicates and duplicates inside decoder-skipped regions by the new structural pass), a stray---starting a second YAML document is an error, and self-referential alias cycles are rejected. YAML anchors and<<merge keys remain fully supported — including merging into typed blocks with standard explicit-key-wins override semantics — but a merge can no longer smuggle an unknown key past a block's schema, and top-level anchor-container keys (x-defaults:style) are rejected: define anchors at their first natural occurrence (e.g.env: &common {...}on one process,env: *commonon the next). String/int shorthand forms are unchanged.
- Two integration-test flakes (#82, PR #84, plan 014). The grandchild
output-capture test polls the captured-output surface for the startup
marker instead of sleeping a fixed 500ms (capture buffers are now
goroutine-safe so polling is race-free); the detached-slow-dependency
test replaces its timed 5s/4s window with a deterministic three-marker
file barrier — the observation can no longer race convergence, and the
subtest got ~4x faster. Both verified 20/20 under
-race.
mkdocs build --strictnow gates every docs-touching PR (#82, PR #84). A build-onlyDocs PR Buildworkflow runs onpull_requestfordocs/**,mkdocs.yml,pyproject.toml, anduv.lock, withuv --lockedso a stale lockfile fails instead of silently re-resolving, and anmkdocsvalidation:block promoting broken anchors/absolute links to build failures. (Path-filtered — deliberately not a required check.)- Configuration reference documents the unified strict-parsing rule, the supported anchor/merge idioms, and the cleartext capture posture.
Capture-by-default (plan 012): a proxy-enabled project now records request/response headers and bodies through the proxy with no extra config, bounded by the disk-budget accounting from #69 and covered by capture-time header/query-param redaction. Bodies are captured verbatim, though — see the Breaking entry.
Dependencies, tasks, and process gating (plan 013, #76): prox can now wait on external resources, run one-shot setup commands, and gate a process's launch on either.
dependencies:,tasks:, andprocesses.<name>.depends_on(#76, plan 013).dependencies:describes an external resource (a database, cache, or other service) with a readinesscheck(tcp/url/cmd, 30s total budget by default including anystart:command's execution time, 1s poll interval) and an optionalstart:command that runs at most once — only when the initial check fails — to bring it up (daemonizing commands likedocker compose up -dare the intended pattern; prox never tears down the external resource itself, though it does kill its own still-runningstart:helper on teardown).tasks:describes a run-to-completion command (a migration, a seed script) that runs once perprox uplifetime, gated on its owndepends_on, with a 60s default run budget (0= unlimited). A process'sdepends_ongates its launch on dependencies and/or tasks (never other processes): the process starts in a newwaitingstate while its targets resolve asynchronously in the background (prox up/prox up -dnever block on this), then either launches normally or settles into a new terminalblockedstate if a required target failed.prox restarton a running gated process/task re-resolves every target — against a fresh config reload — before touching the running instance (fail-before-stop). See the new Dependencies and Tasks guide and the configuration reference.- Status/exit-contract extension for dependencies and tasks (#76, plan
013).
prox statusgains aDependencies:table (state, one-line check summary, last error) and decorates the STATUS column withwaiting(x, y)/blocked(x)naming the unresolved/failed targets; a newBlocked:line mirrors the existingCrashed:line. The exit-1 contract extends to: any process leftblocked, and anydependencies:entry in the terminalfailedstate (awarneddependency does not count — its dependents still run). Precedence for the primary stderr message isproxy-down > crashed > blocked > failed-dependency; all applicable lines still print regardless.GET /statusgains adependenciesarray; each process response gainskind("task"when applicable),waiting_on, andblocked_on. The TUI colorswaiting(amber),blocked(red, bold), andcompleted(gray), with an inline(waiting on: ...)/(blocked on: ...)annotation.
- One-shot
rc=0caveat narrowed to plain processes (#76, plan 013). A bareprocesses:entry that exits0on its own is still markedcrashed— that behavior (plan 011) is unchanged. Atasks:entry, by contrast, maps a naturalexit 0to the new dedicatedcompletedterminal state, which does not tripprox status's exit-1 contract. Usetasks:rather than a plain process for anything meant to run once and exit cleanly.
- Capture is on by default whenever the proxy is enabled (plan 012).
Previously a proxy-enabled project recorded only request/response metadata
(method, URL, status, timing) unless
--capturewas passed; now it also captures headers and bodies by default, spilling large bodies to~/.prox/captureunder the shared disk-budget accounting (#69, 1GiB default across all projects on the daemon). To opt out, setproxy.capture.enabled: falseinprox.yaml, or pass the new--no-captureflag for one run (--capturestill exists for explicitness/compat but no longer does anything a default-on project doesn't already get; the two flags are mutually exclusive). Capture-time redaction (below) covers headers and URLs only — request/response bodies are captured verbatim, so a project whose bodies carry secrets (API keys, tokens, PII in JSON/form payloads) should opt out entirely rather than rely on redaction. Anything that assumes~/.prox/capturestays empty (disk-space checks, backup exclusions, sandboxed CI with a tight tmpfs) should account for the new default.prox proxy statusgainedcapture_available/capture_error(JSON) and aCapture: unavailable (<reason>)line (human output) for the case where the daemon's own capture manager failed to initialize — distinct from a project simply choosing capture off.
- Capture disk budget + record-group FIFO eviction (#69).
proxy.capture.disk_budget(e.g.512MB,2GB) bounds the shared daemon's total spilled-body bytes across every registered project; unset defaults to 1GiB, and the daemon-wide effective bound is the minimum across every capture-enabled project's own budget (a project that leaves it unset contributes the default to that minimum). Once the bound is exceeded, the oldest record's spilled body files are evicted first (FIFO by record group). - O(1) ID index for the request ring (#71).
RequestManager'sUpsert/GetByID/cursor-anchoring lookups no longer linear-scan the ring, keepingprox requestsand the TUI responsive as capture-by-default raises per-project request volume. - Capture-time redaction of sensitive headers and query params (plan 012
D4). On by default whenever a capture config exists (disable with
redact: false); the built-in sets always redact theAuthorization,Proxy-Authorization,Cookie,Set-Cookie,X-Api-Key,X-Auth-Tokenheaders and theaccess_token,refresh_token,id_token,token,api_key,apikey,client_secret,codequery params — including inside aLocation/Refererredirect URL's query string — and are extendable per project viaredact_headers/redact_query_params(these only add to the built-ins, never replace them). Limitation: redaction never touches request/response bodies — see the Breaking entry above.
An exit-contract polish pass (plan 011, PR #77): prox status and prox stop/prox down exit codes now tell the whole truth (crashed children and
wedged daemon teardowns exit 1), a crashed project's proxied routes converge
in about one request instead of up to 30 seconds, and the repo gained a
CLAUDE.md for agent sessions.
Upgrading: as with previous releases, the shared daemon requires an exact version match with its clients. After installing this release, stop the old daemon and restart every project:
prox proxy stop --force, thenprox up -din each project — or runprox upin one project and let the idle-daemon auto-heal replace it. Scripts keying onprox status/prox stopexit codes: read the Breaking entries below.
prox statusexits 1 when any child process is crashed (#72). Previouslyprox statusexited 0 while the table showed acrashedchild, so scripts and coding agents keying on the exit code could miss a dead process entirely. It now exits non-zero whenever any process is in thecrashedstate: the table adds aCrashed: <name>[, ...] — check 'prox logs <name>'.line after the proxy line, and stderr carriesError: N process(es) crashed— unless the shared proxy is also down, in which case the proxy-down error takes stderr precedence while both signals still print (the JSON payload is unchanged — it already reports each process'sstatus). The exit-0 contract is now exact:prox statusexits 0 only when the supervisor query succeeded, no process iscrashed, and any configured shared proxy is reachable — it does not assert every process isrunningorhealthy(starting,stopping,stopped, and running-but-unhealthyall still exit 0). Note the supervisor marks any non-Stop-driven exit ascrashed, including a one-shot child that exits 0 (a migration/seed step): such a project failsprox statusuntil restarted, so one-shot helpers should live outsideprox.yamlor expect a non-zero status. Scripts that need the old behavior must readprox status --jsonand inspect the per-processstatusthey care about — notprox status || true, which would also mask discovery errors and an unreachable supervisor. This consolidates the status exit-code churn started in v0.2.1 (proxy-down → exit 1, #66) into one adoption window.prox stop/prox downexit 1 when the daemon teardown wait times out (#73). Previously, once the process-stop verdict was clean, aprox stop(orprox down) that then timed out waiting (up to ~15s) for the daemon's own state/PID files to disappear still printedStopped processesplus aWarning: the daemon is still finishing shutdownto stderr and exited0. It now returns ashutdown incomplete: daemon still finishing after 15serror and exits1, joining the sameshutdown incompletefamily as the existing survivors contract (v0.1.4, #36) — an unconfirmed daemon teardown is no longer treated as a clean stop. The existing stdout/stderr messages are unchanged, but stderr gains the CLI's standardError: shutdown incomplete: …line. Scripts that assumedprox stop/prox downalways exit0once processes are reported stopped must check the exit code (or parse stderr for theWarning:line) if they need to distinguish a fully torn down daemon from one still finishing up.
- Crashed projects' routes converge in about one request instead of up to
30s (#74). Previously, when a project crashed, the shared proxy kept
serving its routes — returning
502 Backend unavailable— until the periodic stale-PID sweep noticed the dead owner, which could take up to 30 seconds. The daemon now also probes on demand: when a route's backend transport fails and the proxy returns a 502, it checks that route's owningprox upprocess off the request path and, if it is dead, reaps the registration right away, so subsequent requests get a clean404(or fall through to a shared-port neighbor) almost immediately. The probe is flap-safe — it keys on the owner's identity, not the backend's, so a merely restarting or flapping backend under a liveprox upis never deregistered — and cheap under load: a single in-flight probe per project, rate-limited, with a trailing probe so a 502 that arrives right after the crash is never missed. The 30s sweep remains the backstop for cases with no live 502 to trigger on (backend-authored 502s, mid-stream aborts, and dead projects receiving no traffic).
- Repo CLAUDE.md (#75): working conventions for agent-driven development — per-commit gate, plans convention, the port-15561 test-fixture pitfall, the daemon argv decoy, and the release convention.
A hardening pass on lifecycle signals and the requests pipeline: prox up/prox status/prox stop now give trustworthy exit codes and error messages instead of silently degrading, the shared proxy daemon self-heals and isolates projects from each other, and captured request/response bodies decode more content types. Plus a refreshed agent skill and docs.
Upgrading: as with v0.2.0, the shared daemon requires an exact version match with its clients. After installing this release, stop the old daemon and restart every project:
prox proxy stop --force, thenprox upin each project — or just runprox upin one project and let the idle-daemon auto-heal (below) replace it for you.
prox statusexits 1 when the shared proxy is down (#66). Previouslyprox statusonly reflected the project supervisor's own process health, so a dead shared proxy daemon could go unnoticed behind a clean-looking status. It now probes the daemon and printsProxy: DOWN — shared proxy daemon unreachable (proxied routes are dead). Check 'prox proxy status'., exiting non-zero, even when every child process is healthy. Scripts that treatedprox status's exit code as pure process health must account for proxy health too.- Client commands no longer fall back to
:5555(#66).status,logs,stop,start,restart,down,attach, andrequestspreviously dialed the compiled-in127.0.0.1:5555default when they couldn't discover a running instance from.prox/prox.state— silently talking to nothing, or the wrong daemon. They now error with remediation ("run from the project directory, or pass--addr") instead. Scripts invoking these commands from outside the project directory must pass--addrexplicitly. prox up -dexit code is now truthful (was always 0). The detach parent used to printprox started (pid N)and exit 0 immediately after forking, before the child had loaded its config, bound its ports, or registered its routes — so a child that died during startup still reported success.prox up -dnow polls up to 15s for the child to become ready (state file +/health) before exiting 0; it exits 1 with a.prox/prox.logtail on early child death or a never-ready timeout (killing the child on timeout).- Orphan ledger format is a forward-incompatible envelope (#67). The
supervisor's crash-recovery ledger (
.prox/prox.children) is now{"boot_marker":"...","children":[...]}instead of a bare array, so a reboot can't let a PID/start-token collision reap an unrelated process group. A pre-upgradeproxbinary cannot parse the new envelope — it fails to unmarshal and simply skips the reap (never unsafe, just less helpful) until every project has been restarted on this version. Downgrade from this version briefly loses crash-recovery reaping for the same reason. - New dependencies:
github.com/klauspost/compress(zstd decoding) andgithub.com/andybalholm/brotli(brotli decoding), both pure Go.
- Version-skew hard fail with idle-daemon auto-heal. A shared proxy
daemon running a different version than the connecting
proxused to silently fall back to a proxy-less standalone start. Now a version mismatch is a typed error: if the daemon still has registered projects,prox upfails hard, naming both versions, every registered project directory, and the exact remediation; if the daemon is idle,prox upauto-replaces it with a fresh daemon of the current version and prints a one-line notice. Standalone proxy create/start failures are likewise now fatal when a proxy is configured (--no-proxyis the escape hatch) instead of warn-and-continue — no code path starts a project with a silently-disabled proxy anymore. - Registration recovers from a draining daemon. Registering during the
shared daemon's brief shutdown grace used to be a fatal
SHUTTING_DOWNerror whose text said "retry" but nothing retried.prox upnow polls for the old daemon to finish draining (up to 10s), starts a fresh daemon, and re-registers automatically; a second failure is still fatal. - Forwarder self-heal after daemon death (#66). When the shared daemon
dies, every registered project used to reconnect silently forever with no
path back. Projects now detect a prolonged (15s+) connection failure and
re-register with a fresh or recovered daemon automatically (damped to at
most once per 30s), worst case within ~45s — no
prox proxy stop --forcerequired. The daemon's re-registration path was also made idempotent for a live same-identity holder, closing a would-beREGISTRATION_CONFLICTloop. Healing is suppressed while a project is itself shutting down, and a version-mismatch failure surfaces as a status state rather than restarting a daemon that might still be in use. - Boot-marker guard for the orphan ledger (#67). See Breaking above — the ledger now discards itself across a reboot (and on Linux, when it predates this marker) instead of risking a PID/start-token collision.
prox status/GET /statussurface shared-proxy health (#66). A newproxyblock reportsmode(shared/standalone/disabled),daemon_reachable,daemon_version, reconnect/drop/backfill counters, andheal_state. The CLI renders this as aProxy:line (see Breaking).- Stale flag for stuck in-flight requests (#53). A request that has been
in_flightfor more than 5 minutes now also carriesstale: true(stale?in the CLI/TUI) — the completion event may have been lost and the outcome is unknown. Computed at serve time; no protocol or storage change. Long-lived streams and large transfers can legitimately showstale?while still live. - Drop and degradation counters. The request stream's subscriber
channels used to drop events silently on overflow. Dropped-event and
backfill-failure counts are now tracked (atomics) and exposed through the
proxystatus block, so silent request loss becomes visible. - More captured-body content types decoded (#50).
deflate(zlib with a raw-deflate fallback),zstd, andbr(brotli) captured bodies now decode for display, joininggzip/x-gzip; all four are bounded by the same 10MB decode cap. Chained or unrecognized encodings, truncated captures, and corrupt streams still fall back to raw bytes. - Hex preview for binary bodies in the TUI (#50). The request detail
view renders binary bodies as a bounded
hexdump -C-style preview (first 256 bytes, offset + hex + ASCII gutter) instead of an inert[binary data]placeholder. - Cursor pagination for
GET /proxy/requests(#50). A newbefore_idparameter pages strictly older than the given record, returningnext_before_id(the oldest scanned record, so a fully-filtered page still advances) for the next page. An unknown, evicted, or out-of-scope cursor returns410with codeCURSOR_GONE.prox requests(CLI) is unchanged — this is an API-only capability for now. - Per-project request rings and capture caps on the shared daemon
(#49). The daemon's single global request ring and capture cap used to let
one chatty project evict another project's records and bodies, and a
per-project
capture.max_body_sizewas silently ignored on the daemon. Each registered project now gets its own full-capacity ring and itsmax_body_sizeis enforced per request on the shared capture path — one project flooding traffic cannot affect another's history or capture quota.
- Agent skill refreshed to post-hardening reality.
skills/prox/references/api.mdandSKILL.mdnow document 12-char request IDs,since/url_contains,in_flight/stale/captured_size/content_encoding/unavailable_reason, thebefore_id/next_before_id/CURSOR_GONEcursor, theproxystatus block, the no-:5555-fallback rule, and the truthfulprox up -dexit code / version-mismatch behavior. - README, architecture, and reference docs updated to match: the HTTP
API table gains the
/proxy/requestsendpoints;docs/development/architecture.mddocuments the shared proxy daemon, the capture pipeline, and the forwarder bridge;docs/reference/api.md/cli.md/configuration.mddocument theproxystatus block,stale, cursor pagination, the decoder list, and per-projectmax_body_size;docs/guides/shared-proxy.mddocuments self-healing and version-mismatch behavior.
The requests/capture overhaul: the shared proxy daemon now captures request and
response bodies per project, prox requests becomes an ngrok-style inspector,
crashed generations self-heal, and the TUI gains request/log search. Plus a set
of registration/lifecycle hardening fixes and supervisor orphan cleanup.
Upgrading: the daemon requires an exact version match with its clients, so after installing this release, stop the old daemon and restart every project:
prox proxy stop --force, thenprox upin each project (the version gate makes a mismatch loud).
- Body capture in the shared proxy daemon (#40, plan 005). Daemon mode now
captures request/response bodies per project under
~/.prox/capture, gated by a per-projectproxy.captureconfig (enabled,max_body_size). Records are scoped by project directory (not hostname), gzip/deflate bodies are decoded for display rather than corrupted,content_encoding/captured_sizeare recorded, binary bodies are detected integrity-first, and captured bodies are delivered to each project over the SSE bridge. Request IDs are now 12 hex chars. prox requestsas an inspector (plan 005). New agent- and human-friendly filters:--url <substr>,--since <5m|RFC3339>,--min-status/--max-status,--method,--subdomain,--json, andprox requests <id> --bodyto view captured bodies. Responses exposehostname,content_type,content_encoding, andunavailable_reason. Body titles show the content type and pretty-print JSON.- In-flight request visibility (#48, plan 006). Requests are recorded at
response-header time with
in_flight: trueand completed via a defer, so a request is visible while it streams and an aborted stream still records instead of vanishing. Backed by a monotonic in-flight → final state machine (RequestManager.Upsert). - Snapshot backfill on (re)connect (#51, plan 006). A project's forwarder fetches the daemon's existing records concurrently with the live stream on every (re)connect, so reconnecting no longer loses history.
- TUI requests view: explicit cursor + search (#47, plan 007). The requests
view gains an ID-anchored cursor and
/-search that jumps the cursor to matches withn/Nnavigation (wrapping), composing with thesfilter rather than replacing it. - TUI detail live-refresh (#54, plan 007). An open request-detail view refreshes in place when the request completes, instead of going stale.
- TUI logs view: search-match navigation (#56, plan 009). The logs view gains
/-search that jumps the cursor to matching lines withn/N(wrapping) and match highlighting, mirroring the requests view. Note: logs/now navigates (non-matching lines stay visible) rather than filtering — usesfor the live substring filter.
- Crash-restart registration self-heal (#55, plan 007). When a
prox upcrashes without deregistering, a restart of the same project now detects the dead registration and replaces it inline instead of failing with a 409, so a crashed generation recovers withoutprox proxy stop --force. Lifecycle transactions are serialized end-to-end, with a brief bind retry on every registration bind and an epoch-guarded graced shutdown check. - HTTPS certs for domains joining an existing listener (#58, plan 008). A project whose HTTPS port is already bound by another project now gets its domain's certificate generated, so the shared TLS listener's SNI callback can serve it (previously the joining domain's handshakes failed).
- Forced-stop teardown serialized with lifecycle transactions (#60, plan 008).
prox proxy stop --forcenow sets the shutdown flag before responding and drains any in-flight register/deregister under a barrier, so a concurrent registration can't interleave with physical teardown. - PID-reuse can no longer defeat liveness checks (#61, plans 008/009).
Registrations carry an opaque per-host process start token; the stale-PID sweep
and crash-restart self-heal key liveness on
(pid, token)so a reused PID naming a different process reads as dead, and the sweep's removal guard is token-aware so it can't tear down a live restart that reused a crashed PID. - Supervisor: reap orphaned child groups after
kill -9(#59, plan 009). When aprox upis killed withkill -9, the backend process groups it supervised are orphaned and keep holding their ports. The supervisor now persists an ownership ledger and, on the nextprox up, reaps any leftover group it can positively identify (strict start-token match) — so the restarted generation rebinds its ports instead of 502'ing on a wedged orphan.
- Cert generation runs outside the cache lock (plan 009).
EnsureDomainno longer holds the certificate cache lock across the mkcert subprocess and key load, so a joining domain's first-time generation no longer stalls TLS handshakes for other domains on the shared listener. - CI runs on macOS as well as Linux (plan 009). The
testjob now runs on bothubuntu-latestandmacos-latest, exercising the darwin process start-token path on every change.
- Foreground
prox upnow exits non-zero when a process group survives shutdown (#36). Previously foregroundprox up(Ctrl-C or an API shutdown) always exited0, even if a process group could not be reaped and still held its ports. It now exits1with a one-lineshutdown incomplete: …summary (per-survivor detail is written to the log stream). Scripts that asserted a0exit from a foregroundprox upregardless of outcome must be updated. A clean shutdown still exits0.
-
Full-stop failure contract (#36).
prox stop(no arguments) andprox downnow wait for the shutdown outcome instead of firing and forgetting. The daemon reports the process-stop verdict over a newPOST /api/v1/shutdown?wait=truepath, which responds HTTP 200 with{success, waited, failures[]}— 200 even when a group survived, so the structured survivor list (each carrying the stablePROCESS_GROUP_NOT_REAPEDcode) is not discarded. The CLI maps this to exit codes: 0 when everything stopped cleanly (after a brief bounded wait for the daemon's state/PID files to disappear), 1 when a process group survived (each printed as aprocess: errorline), and 1 when the connection drops mid-wait and the outcome is unknown. An older daemon that predates thewaitparameter is detected (the response omitswaited) and the CLI falls back to the legacyShutdown initiatedmessage with exit0. The daemon shutdown stages were reordered so the API server is stopped last (after the supervisor stage and the verdict publish), letting it deliver the waited response, with the launch gate closed first so a lifecycle request during the drain cannot orphan a process. -
Concurrent stops return the same verdict (#32). Two
Stopcalls against the same process (or a daemon shutdown overlapping an in-flight per-process stop) now resolve to the same result: a secondary waiter joins the primary's stop episode and observes its authoritative verdict — including aPROCESS_GROUP_NOT_REAPEDfailure — instead of returning success early when the leader is reaped. A caller whose own context is canceled first still getsctx.Err(). A reap failure now emits aprocess_crashedevent uniformly from both the per-process stop and full-stop paths. -
restartandstartapply the currentprox.yaml(#33). An API-driven (re)start —prox restart <name>, andprox start <name>after aprox stop <name>— now re-reads and validates the whole config file and runs the process with its current config, so the edit → restart → observe loop no longer needs a fullprox stop+prox up. Applied on (re)start:cmd,healthcheck,stop_timeout(the new value governs the next stop; the restart's own stop half keeps the pre-edit budget), and environment inputs (inlineenv, per-process and globalenv_file, including changed file paths). Renames, added/removed processes, andservices/proxy/port changes still requireprox up. The reload is fail-closed: an invalid file (even an unrelated process or the proxy section), a missing referenced env file, or a removed target aborts the (re)start with the existing process left running unchanged, via two new error codes —CONFIG_RELOAD_FAILED(HTTP 422) andPROCESS_NOT_IN_CONFIG(HTTP 409). The config swap is applied atomically inside the start's locked critical section, so a start racing a restart never leaves the running process and stored config mismatched. -
Configurable stop timeout (#35). Two new duration fields control the SIGTERM→SIGKILL escalation budget: global
shutdown_timeout(top-level) and per-processstop_timeout(overrides the global). The effective budget for a process is its ownstop_timeout, else the globalshutdown_timeout, else the built-in10sdefault. The value is the escalation window — a fixed2sis reserved for the SIGKILL phase, so the graceful drain window isbudget − 2s. Values must be greater than2sand at most10m; anything outside that range (including0s/negatives) is rejected at load with a field-named error. The budget is honored end-to-end —prox stop,prox restart(the stop half), and full daemon shutdown — and the effective value is surfaced asstop_timeoutin theGET /processes/{name}response.
- Daemon shutdown now uses per-stage deadlines instead of one shared 10s
budget (#35). Full
prox stop/ Ctrl-C previously wrapped proxy teardown, API-server shutdown, and all process stops in a single 10-second context, so slow proxy/API teardown silently ate into the time available to stop processes — truncating an otherwise-valid graceful drain. Teardown now runs in stages, each with its own deadline computed at shutdown time: the proxy and API server get short fixed deadlines, then every process is stopped concurrently, each on its own configured stop budget (read live, so a per-process budget raised at runtime is respected). With nothing configured, per-process escalation timing is unchanged (10s/2s); the daemon's outer window simply no longer truncates a stop.
- SSE streams are no longer cut at 30 seconds (#42).
GET /api/v1/logs/streamandGET /api/v1/proxy/requests/streamsat in the router's default 30s request-timeout class, soprox logs -f,prox attach, and proxy-request streams silently terminated after ~30s. The two SSE routes are now exempt from the request timeout: streams are long-lived and end only on client disconnect or daemon shutdown. Shutdown now also closes the shared-proxy-daemon request forwarder's subscriber channels, so an attached request stream no longer pins the API server to its full teardown-stage deadline. prox requestsnow discovers the daemon's API address (#43).requestswas missing from the client-command discovery allowlist, so it always talked to the default:5555address and failed against daemons on dynamic API ports (the default) — the same gapstarthad. The allowlist is now pinned by a test so a new client command can't silently miss discovery.prox start <name>now discovers the daemon's API address from.prox/prox.statelike the other client commands; previously it always used the default:5555address and failed against daemons on dynamic API ports (found during #33 verification).- A second
POST /shutdownno longer panics the daemon (#36). The shutdown trigger was a bareclose(shutdownCh), so a duplicate or concurrent shutdown request (e.g. a rapid doubleprox stop) closed an already-closed channel and crashed the daemon. Shutdown is now latched through async.Oncecoordinator, so repeated triggers are safe no-ops. POST /shutdownnow works against a--tuidaemon (#36). The TUI event loop never observed the shutdown channel, so an API shutdown (and thereforeprox stop) was silently inert againstprox up --tui— the request returned 200 but the daemon kept running. The trigger is now routed into the TUI so it quits and runs the normal shutdown sequence.GET /api/v1/logs/streamnow returns a clean JSON error (STREAMING_NOT_SUPPORTED) when the connection cannot stream, instead of writing SSE headers first (#40).- Healthcheck
interval/timeout/retries/start_periodare now honored (#31). Previously onlyhealthcheck.cmdtook effect; the timing/retry fields were silently dropped and replaced by the built-in defaults (10s/5s/3/30s), so a tuned healthcheck ran at the wrong cadence and a slow starter got nostart_periodgrace. Configured values now reach the health checker. An invalid or negative duration failsprox upat load with a clear, process-named error (e.g.processes.api.healthcheck.interval: invalid duration "3x") instead of being silently ignored;0/omitted still means "use the default".
Bug-fix release for the prox restart/stop process lifecycle (#29):
a restart now reloads the process's env_file, and neither stop nor
restart leaves orphaned grandchild processes holding their ports.
restartreloadsenv_file;stop/restartno longer orphan grandchildren (#29, #30). Previouslyprox restartcould report success while running the replacement with a stale environment and leaving the old process's grandchildren alive — holding the listening port, so the replacement failed withEADDRINUSE— andprox stopcould leave the same orphan behind. Now:env_file(global + per-process) and inlineenvare re-read from disk on every start, sostart/restartpick up edited values; a failed reload fails loudly instead of launching with a stale env.stop/restartgate on the whole process group — SIGTERM, a time-based graceful wait, then SIGKILL of the group with reap verification — so a grandchild that ignores SIGTERM is still cleaned up and its port freed.prox stop <name>/prox restart <name>now return a non-zero exit and a typedPROCESS_GROUP_NOT_REAPEDerror when a group can't be reaped, instead of always reporting success.restartstarts the replacement on the supervisor's context, so its health checker survives past the request that triggered it.
- Document the shared proxy daemon.
Release-pipeline + plugin distribution release. Prox is now a
distributable Claude Code plugin (this repo hosts the skill;
previously it lived in charliek/cc-plugins), and the release
pipeline is fully on the cc-plugins:release-workflows convention —
no more per-pipeline PATs, single release-bot App identity for both
the Homebrew tap push and the apt-charliek dispatch.
- Prox is now a distributable Claude Code plugin (#20). This repo is
the canonical home for the
proxskill. Removed fromcharliek/cc-pluginsin a companion change.
- Adopt
cc-plugins:release-workflowsconvention (#21) — prox is the third consumer of the framework (after strix and roost).scripts/release/update-version.shbumps.claude-plugin/plugin.json(delegating to the existingscripts/set-version.sh) with a grep-verify after the delegate so silent sed no-ops fail loudly;RELEASING.mddocuments the per-repo policy + break-glass recovery; multi-targetsanity-check-app.ymlverifies the App reach to homebrew-tap and apt-charliek; the previous CI-drivensync-versionjob is retired (plugin.json bump moves local). - Retire
HOMEBREW_TAP_TOKENandAPT_DISPATCH_TOKENPATs — both replaced bycharliek-release-botApp tokens minted at workflow time (scoped to the target repo viaactions/create-github-app-token'sowner+repositoriesinputs). GoReleaser still readsHOMEBREW_TAP_TOKENfrom env; the workflow now sets it from the App-minted token instead of from secrets. Legacy secrets deleted from the secret store. - Server-side
Verify plugin.json matches tagsafety net inrelease.yaml— catches mismatches between the released tag and what's actually in.claude-plugin/plugin.jsonbefore any artifacts ship. Replaces the deletedsync-versionjob's contract. - Branch protection ruleset on
mainwith the release-bot App + admin role inbypass_actors. update-version.shis now grep-verifying — the wrapper aroundscripts/set-version.shreads the file back and asserts the new version made it in. Silent sed no-ops on malformed manifests now fail loudly.
- Document the Linux (apt) install path + a direct-.deb fallback
(#18, #19) — apt install prox once the apt-charliek repo is added;
apt install ./prox_X.Y.Z_amd64.debfor the no-apt-repo fallback (resolves dependencies, unlikedpkg -i). RELEASING.md(new) — per-repo policy doc.
- Publish
.debpackages foramd64andarm64on every release via GoReleaser'snfpms:block. Install on Pop!_OS / Ubuntu 24.04+ viaapt install proxonce theapt-charliekrepository is added. - Fire
repository_dispatchatcharliek/apt-charliekafter a successful release soapt updatepicks up the new version automatically. Bounded retries on the dispatch call to ride out transient API blips.
- Add
release-snapshotCI job that runsgoreleaser release --snapshoton every PR and validates both theamd64andarm64.debartifacts (Package: prox, payload at/usr/local/bin/prox).
- Add shared proxy daemon for multi-project port sharing
- Add Homebrew tap automation via GoReleaser
- Add Homebrew as recommended install method in README
- Fix WebSocket and SSE connections dying after 30s through proxy
- Fix SSE/streaming support in reverse proxy
- Make proxy port binding failures fatal with actionable errors
- Upgrade deploy-pages to v5 for Node.js 24 support
- Upgrade GitHub Actions to Node.js 24-compatible versions
- Remove legacy plans and replaced watch-pr command
- Remove release command, moved to cc-plugins
- Add HTTP proxy support for dual-stack (HTTP + HTTPS) proxying
- Add request/response body capture for proxy inspection
- Remove
hostsandcertsCLI commands, replaced with documentation - Upgrade golangci-lint to v2 with macOS support
- Fix CI: upgrade golangci-lint-action to v7
- Fix release workflow: upgrade golangci-lint-action to v7 for v2 support
- Add comprehensive tests for request/response body capture
- Add
prox requestscommand to view and stream proxy HTTP requests- Filter by subdomain, HTTP method, and minimum status code
- Stream in real-time with
-f/--followflag - JSON output support with
--jsonflag
- Add
prox start <process>command to start stopped processes - Add
prox stop <process>command to stop individual processes
- Add TTY detection to LogPrinter for clean output when piping
- Add
setcapto install target for privileged port binding - Case-insensitive HTTP method filtering (
--method getworks)
Initial release of prox, a modern process manager for local development.
- Process supervision with automatic restarts and health checks
- Real-time log aggregation with filtering and search
- HTTPS reverse proxy with subdomain routing
- Interactive TUI for monitoring processes and logs
- Background daemon mode with
--detachflag - CLI built with Cobra framework with shell completions
- REST API for programmatic control