Emanuele Micheletti RSS

Proxelar 0.5.0: Sessions, Rules, and More Ways to Capture Traffic

2026-07-22

Proxelar 0.5.0 is out.

This release got a bit out of hand. I started with the boring problem of captures disappearing when the proxy stops and ended up adding sessions, HAR import/export, a real filter language, content-aware body views, declarative rules, a headless API, addon packages, and four new capture modes.

The useful way to explain all of that is not a feature list. It is a workflow: get traffic into Proxelar, find the exchange you care about, understand the body, change or replay it, and keep the result for later. That is what this post walks through.

Captures Finally Survive a Restart

The most obvious missing piece was persistence. Until now, closing Proxelar meant losing the session.

You can now save the complete capture when Proxelar shuts down cleanly:

proxelar --save-session checkout-debug.proxelar.json

Press Ctrl+C when you are done. The file contains completed HTTP flows, WebSocket connections and frames, observed raw TCP chunks, DNS exchanges, UDP datagrams, stable flow IDs, and body-truncation metadata.

Load it on the next run:

proxelar \
  --load-session checkout-debug.proxelar.json \
  --save-session checkout-debug-continued.proxelar.json

The old flows appear immediately, and new traffic is appended to the same in-memory session. This is useful when a bug takes three attempts to reproduce and you do not want three unrelated screenshots pretending to be one timeline.

The native format is versioned JSON. I deliberately kept it readable: it is large, but you can inspect it, diff it, or write a small conversion tool without depending on an opaque database.

There is one important detail: native sessions are not redacted. They are intended to preserve the capture exactly, which means they can contain cookies, authorization headers, tokens, and request bodies. Treat them like credentials. Do not commit them.

HAR, curl, and Raw HTTP

The native format is the best representation of a Proxelar session, but it is not always the format you need. 0.5.0 can import HAR and export three interoperable forms:

proxelar \
  --import-har browser-capture.har \
  --export-har cleaned.har \
  --export-curl replay.sh \
  --export-raw raw-flows/

HAR carries HTTP requests and responses. The curl export writes one command per request. Raw export writes request/response file pairs while preserving repeated headers instead of flattening them into a map.

Common secrets are redacted from these exports by default:

You can opt out with --export-secrets, but the flag is intentionally loud. Most exported captures end up in issue trackers, chat messages, or temporary repositories sooner or later. Redaction should be the boring default.

HAR cannot represent everything in a native session. Raw TCP chunks, DNS/UDP data, and some WebSocket metadata stay native-only. I use HAR when another tool needs the HTTP traffic, curl when I want a reproducible request, and the native file when I may need the full capture later.

Filters Became a Language

The old column:value filter was fine for status:404. It became awkward as soon as I wanted something like "POST requests that failed, except health checks".

Filters now support boolean operators, parentheses, negation, and body/header matching:

method:POST & status:500
host:api.example.test & !path:/health
(status:401 | status:403) & header:authorization
type:json & response_body:error
proto:wss | proto:https

Adjacent terms are an implicit AND, so this works too:

method:POST host:api.example.test status:4

The same parser is used by the TUI, web GUI, session API, and cross-protocol match endpoint. A filter no longer means one thing in the terminal and something slightly different in the browser.

The web GUI showing a boolean content-type filter across captured JSON and JavaScript responses

The main fields are host:, method:, status:, type:, body:, header:, request_body:, and response_body:. Matching is substring-based rather than regular-expression-based. That keeps the common cases readable and avoids turning every filter into a quoting exercise.

Bodies Are More Than a Byte Count

Clicking a JSON response and seeing one long escaped string was not useful. The new content layer decodes transport/content encodings and produces a view based on the media type.

JSON, XML, HTML, forms, multipart bodies, CSS, and JavaScript are formatted for inspection. Declared character sets are decoded. gzip, Brotli, zstd, and deflate bodies are decompressed before display. Safe raster images can render directly in the web interface.

A captured application/json response rendered as formatted structured content

Binary data stays binary. Invalid UTF-8 request bodies open as hexadecimal bytes instead of being converted through a replacement-character string. Protobuf messages can be viewed and edited as wire fields without a schema, and JSON-shaped MessagePack values get a structured editor.

There are boundaries here. Protobuf without a schema has field numbers, not meaningful names. Multipart is split into readable parts, but it is not a full structured multipart editor. A body capped by --body-capture-limit is marked as truncated and cannot be reconstructed from the captured prefix.

When an intercepted or replayed body changes, Proxelar removes stale transfer/content encodings and recalculates Content-Length. This sounds minor, but an editor that leaves Content-Encoding: gzip on a plain JSON body creates bugs that look nothing like the edit you just made.

Rules for the Things That Do Not Need Lua

Lua is still the flexible option, but I was writing tiny scripts for jobs that are really configuration: map this asset directory, point this API prefix at localhost, return a fixed health response.

Two common mappings can now live on the command line:

proxelar \
  --map-local 'https://app.test/assets/=./fixtures/assets' \
  --map-remote 'https://api.test/v1/=http://127.0.0.1:3000/'

For anything more involved, use a JSON rules file:

{
  "rules": [
    {
      "action": "set_request_header",
      "url_prefix": "https://api.test/",
      "name": "x-debug-client",
      "value": "proxelar"
    },
    {
      "action": "mock",
      "url_prefix": "https://api.test/health",
      "method": "GET",
      "status": 200,
      "headers": [{ "name": "content-type", "value": "application/json" }],
      "body": "{\"ok\":true}"
    }
  ]
}
proxelar --rules rules.json

Rules run in file order. Header changes can accumulate; the first matching rule that creates a response wins. Available actions are map_local, map_remote, redirect, mock, set_request_header, and remove_request_header.

Map-local paths are constrained to the configured directory. ../ traversal and symlink escapes are rejected. A local development convenience should not quietly become a file server for the rest of your machine.

My rule of thumb is simple: use rules for static routing decisions, Lua when the decision needs code, and interactive intercept when I only need to change one request.

A Headless API

The web interface already needed a server for live events and commands, so 0.5.0 exposes the useful parts as a documented JSON API:

export PROXELAR_TOKEN='local-development-token'

proxelar -i api --api-token "$PROXELAR_TOKEN"

Every request needs a bearer token:

curl \
  -H "Authorization: Bearer $PROXELAR_TOKEN" \
  'http://127.0.0.1:8081/api/v1/flows?filter=method:POST%20%26%20status:500'

The API can read the session, query flows, fetch decoded content views, replay requests, clear traffic, toggle intercept mode, and resolve pending intercepts with forward/drop/modify decisions. Binary modified bodies can be sent as byte arrays, and headers can be represented as an ordered list when duplicate order matters.

This makes shell scripts and local test harnesses possible without pretending the browser GUI is an automation protocol.

It is still a local developer API. There are no user accounts, TLS termination, rate limits, or multi-tenant permissions. Keep it on loopback. If it has to cross a network, put it behind an authenticated TLS tunnel and treat the token as a credential.

The browser GUI uses a separate login token. Proxelar opens a URL with the token in the fragment, exchanges it for an HttpOnly, SameSite=Strict cookie, and removes the fragment from browser history. The API token is not embedded in the downloadable JavaScript.

More Ways to Get Traffic In

Forward and reverse proxy modes are still the normal choices, but some clients cannot be configured with an HTTP proxy. 0.5.0 adds several narrower capture modes for those cases.

SOCKS5

proxelar -m socks5 -p 1080

curl --socks5-hostname 127.0.0.1:1080 http://example.com/

The SOCKS5 listener accepts IPv4, IPv6, and domain CONNECT targets. HTTP is inspected, TLS goes through the normal local-CA flow, and unknown protocols are recorded as directional raw TCP chunks.

There is currently no SOCKS authentication. Bind it to loopback unless an unauthenticated listener is exactly what you intended.

DNS

proxelar -m dns -p 5353 \
  --dns-upstream 1.1.1.1:53 \
  --dns-map api.example.test=127.0.0.1

DNS mode records UDP queries and responses, forwards normal lookups to the configured resolver, and can synthesize A/AAAA answers. I use it to point a real client at a local API without changing the client's hostname configuration.

It is plain UDP DNS, not DNS-over-HTTPS.

Fixed-Target UDP

proxelar -m udp -p 9001 --target upstream.example:9000

This mode forwards each incoming datagram to one known upstream and records both directions. It is deliberately request/response-oriented: one target and at most one response per request, with a five-second no-response result. It is useful for small known UDP protocols, not as a general UDP router.

WireGuard

proxelar -m wireguard -b 0.0.0.0 -p 51820 \
  --wireguard-endpoint 192.168.1.10:51820

On first start, Proxelar writes an owner-only client configuration to ~/.proxelar/proxelar-wg.conf. The empty TUI and authenticated web GUI show the same profile as a QR code, while terminal mode prints it at startup. Scan it from the WireGuard app or import the file directly. The QR disappears from the interactive interfaces after the first captured event.

The empty Proxelar TUI showing the WireGuard client profile as a high-contrast QR code

The proxelar-wg name is intentionally short because Android limits WireGuard interface names to 15 characters. The QR contains the client private key, so show it only on a trusted screen.

TCP is reconstructed in userspace and then follows the normal HTTP, TLS, WebSocket, or raw-stream paths. Port 53 uses the DNS configuration and overrides described above.

This is the mode I reach for when an app has no proxy setting but can use a VPN profile. It currently generates one client identity per CA directory, and UDP forwarding is still aimed at request/response traffic. QUIC/HTTP/3 interception is not supported.

Chaining Through Another Proxy

Outbound connections can go through an HTTP CONNECT or SOCKS5 proxy:

proxelar --upstream-proxy http://proxy.example:8080
proxelar --upstream-proxy socks5://127.0.0.1:9050
proxelar --upstream-proxy http://proxy.example:8080 \
  --upstream-proxy-auth 'user:password'

Chaining applies to ordinary forwarding, reverse mode, and replay. Credentials passed on the command line may be visible to local process inspection, so use a dedicated low-privilege account rather than your important password.

Lua Addons Instead of Loose Folders

Loose --script files remain the fastest way to iterate. They now hot-reload when the entrypoint changes, keeping the last working version if the new script has an error. WebSocket frames can also pass through an optional Lua hook for modification or dropping.

For something I want to keep or share, there is now an addon package format:

proxelar addon verify ./header-tagger
proxelar addon install ./header-tagger
proxelar addon list
proxelar --addon header-tagger

An addon has an init.lua entrypoint and a versioned proxelar-addon.json manifest. The manifest declares its version, hooks, requirements, and SHA-256 digest for every file. Installation rejects traversal, symlinks, undeclared files, and digest mismatches before copying the package into the local catalog.

This is not an online marketplace. It is a boring, inspectable package boundary for Lua code you already have on disk. That is enough for now.

Launching a Browser Without Changing System Settings

For browser-only debugging, this is the shortest path:

proxelar -i gui --launch-browser

Proxelar finds a Chromium-family browser and starts an isolated profile configured to use the proxy. Your normal browser profile, extensions, cookies, and proxy settings stay untouched.

The isolated profile is intentional. Installing a local CA and changing global proxy settings just to inspect one site is a lot of state to remember to undo.

A Few Things That Changed Underneath

There are smaller changes that matter even if they do not get their own button:

HTTP/2 clients are accepted, but inspected requests are currently normalized and forwarded upstream as HTTP/1.1. HTTP/3 is not intercepted. Certificate-pinned applications will reject the generated certificates, and Android applications must explicitly trust user-installed CAs.

Those limits are more useful written down than hidden behind a vague "supports HTTPS" bullet.

Getting Started

Install or update Proxelar:

brew upgrade proxelar
# or
cargo install proxelar

For a quick browser session with persistence:

proxelar -i gui --launch-browser \
  --save-session debug.proxelar.json \
  --export-har debug.har

For a quick SOCKS test:

proxelar -i gui -m socks5 -p 1080
curl --socks5-hostname 127.0.0.1:1080 http://example.com/

For a local API with a static mock:

proxelar -m reverse \
  --target http://127.0.0.1:3000 \
  --rules rules.json \
  --save-session local-api.proxelar.json

The full documentation is at proxelar.micheletti.io, and the complete changelog is on GitHub.

I would start with sessions and the filter language. They are the least dramatic features in the release, and they are the ones that changed my day-to-day use the most. Capture something once, narrow it down without scrolling through 500 rows, and keep the useful part for tomorrow.