Repository navigation
Releases: johnfink8/brackish
Release list
v0.7.2
[0.7.2] - 2026-05-26
Optional TLS for cross-machine use — bring your own self-signed cert; the client pins it by fingerprint, no CA.
Added
- BYO-TLS on the TCP bind.
brackish serve/up --tls-cert <pem> --tls-key <pem>serve HTTPS on the bind; the Unix socket stays plain HTTP. The client pins the cert by SHA-256 fingerprint, carried in thebrackish connect … --tls-pin sha256:…line thatinvite/serve --inviteprint and verified before the first token-bearing request — so cross-machine traffic is encrypted and MITM-resistant without provisioning a CA.brackish tls genwrapsopensslto mint a self-signed cert+key (clear error + the manual command if openssl is absent).
Removed
brackish installno longer editssettings.jsonat all. Dropped the stubbed-offUserPromptSubmitinbox hook (script +activate/deactivate/hook-snippetwiring) and the--permissionallow-rule insertion.installnow just copies the skill dir;uninstallremoves it. Sync is the foregroundstatus/naploop. If you want Claude to skip per-command approval for brackish, add aBash(brackish *)allow-rule to yoursettings.jsonyourself — brackish won't write it for you.
v0.7.1
[0.7.1] - 2026-05-26
0.7.1 standardizes the CLI grammar (a breaking flip to verb-first) and makes every command atomic. On top of that: renegotiation got proper primitives — counter and a negotiated retract — and turns now deliver as coherent batches. Most of it came out of the e2e trials.
Added
brackish counter <noun> <id> --file <f> --rationale "<why>"— atomically rejects the peer's current proposed version and proposes your replacement in one transaction (no rejected-with-no-counter window). Replaces the manual reject-then-propose two-step, and reads in the log as a counter rather than a refusal.brackish accept <noun> --target … --include-dependencies— opt-in: also accepts the still-proposed schemas the targets$ref(transitive closure), in the same atomic batch. Default off; without it, accepting an endpoint whose schema isn't accepted yet is refused withaccept_orphans_ref, which names the missing schema and how to accept it.brackish deliver <doc>+ turn-delivery model. Your events (proposes/accepts/rejects/retractions/messages) are held until you deliver them, then surface to the peer'sread/inbox/waitas one batch.nap/waitimplydeliver; your own moves still show instatusimmediately. (events.delivered_atcolumn; migration backfills existing events as delivered.)
Changed
- BREAKING: the CLI is verb-first —
brackish <verb> <noun> [identity](accept endpoint GET /users, notendpoint accept). The document is the--docoption (defaults to the sole doc), no longer a positional.proposeis file-only (--file; the inline field/response/security builders are gone).--version/--expected-version→--rev/--expected-rev.propose-batch <doc> --manifest→propose --manifest. Variadicaccept <doc> A B C→accept <noun> --target A --target B. Standalone convention lint removed (propose --manifestpre-flight +validatecover it). - Every command is atomic — all-or-nothing. Batch accept now commits the whole set in one server transaction (was a client-side loop that could leave a partial accept); a mutually-referencing set accepts together, and any failure rolls back with nothing written.
propose --manifestandcounterlikewise. retractis now a negotiated propose/accept lifecycle under theretractionnoun.brackish propose retraction --endpoint X --schema Y --rationale "<why>"opens a grouped removal; the peer accepts it (tombstoned atomically, validated fully-valid-after) or rejects it. Verb-first:propose | accept | reject | withdraw | list retraction. Newretractionstable +retraction_*events. Breaking vs 0.6.1's unilateralretract(which removed immediately).brackish readinfers the sole document when<doc>is omitted (matchingstatus), and no longer truncates rejection/acceptance reasons or deltas.brackish installno longer wires aUserPromptSubmithook — skill (+ optional--permission) only; the foregroundstatus/naploop is the sync model.activate/deactivateare no-ops for now.
v0.6.1
[0.6.1] - 2026-05-24
Re-negotiation escape hatches (issue #2): removing accepted artifacts, and a read-only validity check so you stop reverse-engineering the validation model by proposing things.
Added
brackish retract <doc> --endpoint "GET /a" --schema Foo [--convention] [--reason ...]removes already-accepted artifacts (atomic, all-or-nothing). The post-removal doc must still validate, so a wedged doc — accepted-but-invalid under a newer OpenAPI check — is fixed by retracting the invalid set together, then re-proposing clean. Retraction is a tombstone version (history preserved), effective immediately; the peer seesartifact_retractedevents. New artifact statusretracted+ schema migration (v3→v4) widening thestatusCHECK.brackish validate <doc> [--manifest <file>]dry-runs the server's OpenAPI 3.1 assembly + meta-schema validation and writes nothing. Bare form checks the current accepted doc (diagnoses an already-invalid doc);--manifestpreviews proposing a whole set together, exactly aspropose-batchassembles it.
Changed
propose-batchfailure output no longer reads as a per-item, partially-applied sequence. An atomic rejection now says the batch was rejected and nothing was written, instead of listing "remaining (not attempted)" items that implied a stop-on-first commit.spec_invalidrecovery hint now distinguishes "your spec is wrong" from "the doc is already invalid from other artifacts" and points atbrackish validate/brackish retract.<kind> lintwarns on OpenAPI 3.0nullable(removed in 3.1 — the meta-schema silently ignores it, so it passes server validation but 3.1 codegen treats the field as non-nullable). Names the field path; suggeststype: [<type>, 'null'].
Fixed
<kind> show <doc> <id>routes the status header/divider to stderr, leaving only the spec body on stdout — soshow > file.yamlwrites a clean file, as the skill always claimed. Both versions present → bodies joined by a---YAML document separator on stdout.
v0.6.0
[0.6.0] - 2026-05-22
Security-and-correctness pass plus a CLI refresh driven by real chat-app trials. The eleven security fixes are the headline — XSS, prompt-injection, default-bind exposure, error-mapping holes, partial-commit semantics, capped diff resolution, plaintext token storage, missing per-document ACLs, query-string token leakage, missing rate limiting, and the absence of explicit "not production-hardened" framing. Each has a red test that demonstrates the targeted exploit/bug before the change and turns green after. After the security pass, seven trial runs surfaced a second batch of model-facing CLI polish — show simplified, --rationale symmetric on reject, status annotates blocked-on refs, skill ships a verb cheat sheet.
Security
- XSS in
/ui/<doc>(script-tag breakout): inlined spec/rationale/events JSON now escapes<and U+2028/U+2029 so peer-supplied strings can't close the outer<script>and execute as HTML. - Prompt-injection of the local Claude via the UserPromptSubmit hook: inbox previews neutralize angle brackets in peer-supplied text (message bodies, rejection reasons, delta strings), and the hook moves peer content out of the
<system-reminder>block into a labeled<untrusted_user_content>block. - Tokens are stored hashed (sha256) at rest. Both persistent peer tokens and outstanding invite tokens migrate in place on first startup; the raw token only ever lives on the peer side. Malformed
expires_atcolumns now fail-closed (treat as expired) instead of fail-open viaDate.parse() === NaN. - Per-document ACLs gate every doc-scoped TCP endpoint. Document creators are owners; other parties access docs only if explicitly granted (via
brackish doc grantorbrackish invite --grant <doc>). Socket peers retain peer-trust. - Token-in-URL-query (
?token=) auth fallback removed. Browser UI is now reachable only on loopback TCP, without auth — anyone who can connect to127.0.0.1already qualifies as a local user. Cross-machine browser UI is explicitly not supported; ssh-forward to loopback, or use the CLI. Tokens never appear in URLs, logs, browser history, or Referer headers. - Rate limiting on
/connect(10/min per source IP) and failed bearer auth (20/min per IP). Socket peers bypass.
Added
brackish doc grant <doc> <identity>/brackish doc revoke <doc> <identity>/brackish doc members <doc>for managing per-document ACLs.brackish invite --grant <doc>(repeatable, also accepts comma-separated) — the redeeming peer automatically becomes a member of each named doc.brackish status <doc>annotates each row with(blocked on: X, Y)when a proposal's$refs point at schemas that aren't accepted yet — so the model sees ref-blocked artifacts before attempting an accept that would fail validation.--rationale "<text>"on<kind> rejectas an alias for the positional reason — symmetric withaccept --rationale. Either positional or flag form works; passing both errors.SKILL.mdverb cheat sheet: one-line description + sample for every brackish command type, grouped by daemon/identity, ACL, propose, accept-reject-withdraw, diff, lint, visualize, wait. Comprehensive based on what models actually reach for in trials.- README: new "Security model" section describing the trust boundary, transport assumptions, and what brackish is not.
servestderr banner: positive "loopback only — NOT externally reachable" line on127.0.0.1binds; warning banner naming the address on any non-loopback bind. Suppressible viaBRACKISH_QUIET_BIND_WARNING=1.
Changed
- Bare
brackish serve --bind(andbrackish up --bind) defaults to127.0.0.1:11442. Pass--bind 0.0.0.0explicitly to expose on the LAN. Breaking for cross-machine setups using the bare flag. <kind> show <doc> <id>always returns whatever's live, tagged by status — accepted, proposed, or both if both exist (the latter case adds adelta vs acceptedannotation on the proposed). The--proposedflag was removed (always-tagged output makes it redundant);--fullis accepted as a silent no-op for callers that still pass it.app.onErrormapsHttpError(query-param validation) andZodError(body / param zod validation) to 400 instead of 500. Surfaces the actual validation message instead of "internal server error".propose-batchis now truly atomic: a newStore.batchProposewraps all per-artifact proposes in a single SQLite transaction with savepoints. Partial commit is impossible — a mid-batch failure rolls back every earlier propose. The response shape no longer carries a partialsucceededfield on error.- Diff endpoint resolves the default
--toviaMAX(version)instead of linearly probing versions 50→1, so artifacts with more than 50 versions diff correctly without an explicit--to. - Skill
SKILL.mdandserver.mdteach the new bind default: agents now ask the human about connectivity intent (loopback vs LAN-reachable) before binding cross-machine.
Fixed
- Diff endpoint key-case mismatch: server's diff lookup used the projection's lowercase
operationKeyinstead of the store's uppercaseoperationIdentityKey, causing every endpoint diff to 404. Caught by a new full-journey integration test that walks the entire skill flow (doc-grant through visualize) end-to-end in ~100ms.
Breaking
--binddefault switched from0.0.0.0to127.0.0.1. Re-launch cross-machine daemons with--bind 0.0.0.0explicitly.- TCP
?token=query-string auth fallback removed. Browser callers visit/ui/<doc>on loopback (no auth required there); CLI callers must use theAuthorization: Bearerheader. - Per-document ACL enforcement on TCP. Existing docs auto-grant their
created_byas owner via migration; pre-0.6.0 TCP peers other than the doc creator must be added explicitly withbrackish doc grant. - Token-at-rest format change. The migration rehashes existing tokens in place on first startup; peers don't need to re-redeem. The raw
tokencolumn is replaced bytoken_hash. Store.createInviteinterface grew an optionalgrantDocsarray; external Store implementations need to handle it (or ignore via default[]).- The
propose-batchHTTP response on failure no longer includes asucceededlist — the operation is all-or-nothing. <kind> show --proposedflag removed. Bareshownow returns whatever's live (tagged), so callers no longer need to pick.--fullis accepted as a no-op for back-compat with callers that habitually pass it. JSON output shape changed from a single envelope to{accepted?, proposed?, deltaVsAccepted?}.
v0.5.3
[0.5.3] - 2026-05-22
Added
--rationale "<text>"onaccept(schema/endpoint/convention; single and batch). The reason rides on theartifact_acceptedevent and renders in the sidebar.--tail Nonbrackish read— peek at the last N events without advancing the cursor; cheap end-of-log scan with no cursor math.harness/extract-demo.ts+run-trial --demo-data <path>for regenerating thebrackish demomove log from a finished trial; demo data refreshed from a 6-round, fully-settled chat-app run (11 of 14 accepts carry rationale).
Changed
skill/server.mdnow teaches<kind> diff --from N --to Mso backend Claudes find it;skill/propose.mdreconciles the batch threshold (3+ artifacts of any mix, with convention bundled unless contentious).- Hook reminder is informational rather than directive; surfaces
brackish deactivateas the off-ramp once a negotiation has wrapped. brackish demoUI: sidebar is a single chronological timeline (one card per event, color-coded by kind), accepts surface their--rationaleinline, both panes scroll independently within the viewport.- README: added a demo screenshot.
v0.5.2
v0.5.0
Added
- Authoritative OpenAPI 3.1 meta-schema validation on every propose and accept. The server runs
@seriousme/openapi-schema-validatoragainst the projected assembled doc and refuses to enter a state where it would be invalid: dangling$refs, missing required fields (e.g.info.version,securityScheme.schemeontype: http), malformed parameter / response / requestBody shapes are rejected at the moment of writing instead of surfacing later as codegen failures. - Atomic batch endpoint
POST /documents/:name/propose-batch. Accepts a coordinated set of artifacts (convention + schemas + endpoints), assembles them into the projected wide doc, validates once, commits all-or-nothing. Mutual references and out-of-order forward refs work in a single request — order within a batch no longer matters. spec_invaliderror code carrying a structuredissuesarray (severity / field / message). Surfaced through the client recovery hint and CLI rendering so Claude knows exactly what to propose first when a ref doesn't resolve.- Releases publish via GitHub Actions trusted publishing (OIDC). Published artifacts carry an npm Provenance badge linking back to the workflow run that built them; no long-lived
NPM_TOKENlives on the laptop or in GitHub secrets.
Changed
- Breaking: propose-time and accept-time semantics. Each per-artifact propose validates the projected wide doc (accepted + currently-proposed + this propose); each per-artifact accept validates the projected accepted doc (accepted-only + this accept applied). Specs that 0.4.x accepted — e.g. an
http-typedsecuritySchememissing the requiredschemefield, or a schema referencing another schema not yet in the doc — now return 400spec_invalid. Pre-flight withbrackish <kind> lint <file>to catch local shape mistakes; the cross-artifact ref check requires doc context and runs on the server. proposeBatchFromManifestnow POSTs the whole manifest as a single atomic request rather than looping individual proposes. Failure semantics are stricter: a parse or lint error means no propose is sent (no partial success up to the failing item).brackish visualizeand the accept-time validator share one assembly code path (projectDocument(..., 'accepted')). The doc rendered for downstream codegen and the doc the arbitrator validates against are structurally identical — no drift.brackish <kind> lintnow runs zod parse + brackish-specific cross-field checks only (path placeholders ↔ parameters, security refs within a convention,x-brackish.namingenum). Full meta-schema validation moved to the server since it needs doc context for ref resolution. The lint output is best-effort pre-flight; the server is the arbitrator.
Fixed
- The validator's
errorsfield can beErrorObject[]orstring(the latter for unresolved-ref failures). The string path was being silently dropped to an empty array, so invalid specs with dangling refs slipped through. Both shapes now surface. - The daemon's
/healthzand/whoamiendpoints reported a stale hardcoded version (0.3.0) regardless of the running package version. Same shape as 0.4.2'sCLI_VERSIONfix; now read frompackage.jsonat build time via the static JSON import so the two can't drift.
v0.4.1
Fixed (0.4.1)
package.jsonbinfield had a leading./… (0.4.0 never published as a usable version; 0.4.1 is the first that works on
npm install -g).repository.urlnormalized.
Initial public release (0.4.0)
Initial public release.
The protocol
- OpenAPI 3.1 propose / accept / reject / withdraw lifecycle for three artifact kinds:
endpoint(Operation Objects),schema(component schemas), andconvention(the document-level info / servers / securitySchemes singleton). - Optimistic concurrency on every propose:
--expected-newfor first proposals,--expected-version <N>for revisions,--forcefor explicit stacking. Race losers get aversion_in_flightorversion_mismatch409, not silent overwrites. - Compact JSON-Patch deltas (
+responses.409,~oneOf.6.properties.code.enum) carried on each artifact version and surfaced in event summaries, so peers can reason about changes without re-pulling the full body. x-brackishextension namespace on Operations and Conventions:idempotent,sideEffects,timing,streaming,frames,eventTypes,naming. Consolidated under one key per object so it reads as a single metadata block and never collides with HTTP headers.- Document-level convention inheritance: endpoints inherit
securityfrom the convention's--global-securityautomatically; per-endpoint--no-inherit-securityemitssecurity: [](the OpenAPI spelling for "no auth required").
CLI
- Per-kind verbs:
propose,accept,reject,withdraw,show,list,diff,lintfor each of endpoint / schema / convention. - Batch accept:
brackish schema accept <doc> A B C ...(variadic),brackish endpoint accept <doc> --target METHOD:PATH ...(repeatable). Stop on first failure with a what-succeeded / remaining summary. - Batch propose:
brackish propose-batch <doc> --manifest <file>parses + lints every artifact locally before sending, in convention → schemas → endpoints order.--dry-run(alias--lint-only) runs the pipeline without contacting the daemon. - Local lint pre-flight:
brackish endpoint lint <method> <path> <file>(andschema lint,convention lint) validates a YAML or JSON file against the same zod schemas the server uses on propose, plus cross-field checks (path placeholder ↔ parameters consistency, security ↔ securitySchemes references,x-brackish.namingenum). Catches the cheap class of rejections without a network round-trip. brackish status [doc]: bucketed view by ownership (awaiting peer / awaiting me / accepted / needs attention). Doc argument is optional — a single active doc auto-resolves; multiple docs print a per-doc summary. Single-doc view appends anext:hint nudging the most likely verb.brackish readadvances per-identity cursors automatically.brackish wait <doc>long-polls.brackish nap [--seconds N]blocks then snapshots the inbox once.brackish watch [--all]foreground-tails events.brackish inboxsummarizes all docs with new activity for the caller; self-authored events are filtered.brackish visualize <doc>renders the assembled OpenAPI document intext,markdown,html(Swagger UI + rationale sidebar),openapi(YAML), orjson. Warns when the convention is not accepted and a stub Info block is being substituted.brackish demoruns an end-to-end sample negotiation in an ephemeral sandbox and prints a ready-to-open browser URL.
Transport and identity
- Unix-socket transport for same-machine use: peer-trust via an
X-Brackish-Identityheader. No tokens, no ceremony. - TCP transport for cross-machine use: bearer-token auth. Bootstrap via
brackish invite <peer>on the server side (mints a one-time token + prints a/brackish connect URL --token T --identity Nline) andbrackish connect URL --token T --identity Non the peer side (redeems the invite, writes persistent config). Dual-bound server detects per-connection auth mode automatically.
Claude Code integration
- Bundled skill installed by
brackish installinto~/.claude/skills/brackish/(global) or./.claude/skills/brackish/(project). Split into a slimSKILL.mdentry point plus role-specific subfiles (server.md,client.md,propose.md,patterns.md) loaded on demand. UserPromptSubmithook surfaces pending inbox events at the start of each turn.--permissioninstall flag adds aBash(brackish *)allow-rule so Claude can run brackish commands without per-command prompts.brackish activateandbrackish deactivatetoggle the hook without uninstalling the skill, for switching between negotiating a contract and implementing it.brackish install/uninstall/hook-snippetfor full skill + hook + permission management. Interactive scope prompt or explicit--global/--local/--scopeflags.
Server, storage, and infrastructure
- Single-binary architecture:
brackish serveruns in the same Node process as the CLI;brackish up/downmanage a backgrounded daemon via a PID file. - SQLite via
better-sqlite3with WAL mode. Events are append-only; documents + per-artifact state are projections. - Long-poll wait endpoint for cross-process notification.
- Browser UI at
/ui/<doc>: Swagger UI for the assembled OpenAPI document, plus a sidebar showing every artifact's negotiation history (proposed / accepted / rejected events with rationale).
Output
- Compact text by default;
--jsonfor structured output on every verb. - Instructive output: success lines for propose, reject, and
doc newappend a one-line→hint pointing at the next likely move. Error responses map known codes (version_in_flight,version_mismatch,cannot_accept_own,artifact_not_pending,artifact_not_found,document_not_found,invite_*) to actionable recovery suggestions. - Exit codes:
0for success (including a timed-outwait),1for operation errors (4xx),2for config / auth / connection errors.
Requirements
- Node.js 22 or newer.
preinstallscript enforces this with an instructive message pointing at nvm.