Skip to content

Escaping and delimiting text shown to an operator ​

This document specifies the form in which text reaches an operator, with its enforcement points: the display sanitization escape format and the one place each fragment takes it, the operator-chosen fragments rendered as typed, the partner-origin text type and the abort-route layout, the decode description split per consumer, the routes that still escape twice and the console's relay, and the compatibility diagnostic value delimiting. It is the implementation-level complement to the Console and log hygiene overview in SECURITY_DESIGN.md, which covers the threat and the display boundary's rules. It does not cover the private-key redaction, the message display budgets, or the consent summary display types (see DISPLAY_REDACTION.md). Intended readers are security auditors, implementors, and anyone writing an error, a log line or a prompt.

Every net-new control specified here is subject to the explicit pre-release security review required by CONTRIBUTING.

Display sanitization escape format ​

The display sanitizer escapes a string for operator-facing output by replacing every byte or code point that is not printable ASCII - C0 and C1 control characters, DEL, and any non-ASCII code point - with a hex escape: \xHH for a code point <= 0xFF, \uHHHH for a code point <= 0xFFFF, and \u{HHHHH} or \u{HHHHHH} (unpadded) for an astral code point above 0xFFFF. A literal backslash is doubled (\\), and printable ASCII (0x20 to 0x7E) is otherwise left unchanged. The output is truncated to a bounded maximum length with a visible truncation marker. It is a one-way display transform: the escaped form is never parsed back and never used to build a filesystem path. Matching and routing decisions use the original bytes, with one deliberate exception: the console's relay of CLI events (apps/web/src/jobs/cliDriver.ts) escapes an event's keys, compares the escaped key against the name of the field the relay itself owns (so a source key that would land as that name is dropped), and uses escaped keys as the keys of the relayed object. That use is fail-safe, since escaping can only make a key differ from the source's.

The transform is applied once per fragment, at the sink that shows it, to the fragments the operator did not choose - the boundary the next paragraph draws - on every route the assignment below governs; the routes that do not yet meet it are enumerated at the end of this section rather than left implicit. Every escape doubles a literal backslash, so a value escaped at two altitudes reaches the operator with a backslash count that no longer matches the bytes it names - one backslash in a partner filename shown as four, in a message telling the operator to go and delete that file. The two altitudes are therefore assigned rather than left to each call site: a fragment interpolated into an Error message or cause is composed raw and escaped once by sanitizeErrorForDisplay where the error is rendered (which also caps each cause-chain link and strips PEM/OpenSSH private-key blocks). A value that reaches a log, a console, or a UI element without ever becoming an Error is escaped by sanitizeForDisplay at that call site, which is its sink. A fragment bound for a log or prompt sink takes both halves in one call, redactAndSanitizeForDisplay, which redacts and then escapes -- the same single escape, not a second altitude.

The escape applies to the side of the fragment boundary the operator did not choose. Doubling a literal backslash is what keeps the \xHH tokens unambiguous over bytes somebody else chose, and it is wrong over a path the operator typed: C:\data\in.csv reaches them as C:\\data\\in.csv, a path they cannot copy back into a command. So the two altitudes above assign a treatment per FRAGMENT rather than per value. A fragment nobody marked is escaped, which is the default and the reason a partner-chosen value keeps its escape by doing nothing. A fragment the composition site marks as the operator's own -- operatorSuppliedText (packages/core/src/utils/operatorSuppliedText.ts) -- is rendered as they typed it by renderOperatorSuppliedText (packages/core/src/utils/sanitizeForDisplay.ts), which replaces the control class with the same printable <HH> marker the control replacement emits, replaces the U+2028 and U+2029 line separators and any lone surrogate with a <HHHH> marker of that same shape, and leaves every other code point standing. A message assembled from both sides therefore escapes the part of itself the operator did not choose. What the render gives up against the escape, over bytes the operator chose, is the bidi and confusable neutralization. What it keeps out is the class that drives a terminal -- an ESC opening an ANSI sequence, a break spoofing a log line -- along with the two line separators \p{Cc} does not hold and a reader still breaks a line on, and a lone surrogate, which would leave what the sink is handed non-well-formed. It keeps them out because a path can be copied into a configuration out of an invitation the partner wrote, and each renders to a marker with no backslash for the sink to escape.

The mark reaches each sink on the route that sink is on. On the ERROR route the composition site builds its message through messageWithOperatorText and marks the error with keepOperatorSuppliedText, which records the message's spans and their origins; sanitizeErrorForDisplay then renders that link span by span, redacting each span, escaping the ones nobody marked, and spending one per-link budget (COMPOSED_MESSAGE_MAX_DISPLAY_LENGTH) across all of them, so a partitioned link is bounded exactly where an unpartitioned one is. Redacting per span narrows the fail-closed dangling rule the way a composition site does: a BEGIN marker in the path costs the path and not the instruction composed behind it. On the LOG, console and prompt route the call site takes redactAndRenderOperatorSuppliedText, the operator-origin sibling of redactAndSanitizeForDisplay; the log prefixer's own per-argument strip is unchanged and runs over both renderings alike. Both entry points take the MARK rather than a string, so on either route the claim "the operator chose these bytes" is a value the composition site builds and a plain string or a PartnerOriginText is a compile error at the sink; a value that reaches one unmarked regardless is escaped rather than rendered as given. Two limits are measured rather than assumed. The mark is a registered symbol property, so no value a partner sends can ask for the treatment -- no parse produces a symbol key -- and it is read only where its spans join back to the link's own message, so a mark describing other text costs a doubled backslash rather than a wrong rendering; a link that also kept its own line breaks renders through that form instead, which escapes every span. And a boundary holding a chain as rendered TEXT has no mark to read: sanitizeErrorChainLinks escapes each link whole, so an operator's path relayed to the console arrives there with its separators doubled. packages/core/test/utils/operatorSuppliedText.test.ts pins the rendered output of a message carrying an operator path beside a partner-chosen fragment, and packages/core/test/utils/logger.test.ts pins the two on one log line.

Which sites are on the mark is held by a check over the names it can recognize rather than by a roster. An ESLint ban over apps/cli/src (eslint.config.mjs) reports a name composed into an Error message, a cause, or a log, console or prompt call without the mark, since the name is the only origin evidence text alone holds -- the config runs no TypeScript program. The names it matches are exactly three shapes: a name ENDING in Path, File, Dir, Directory or Folder, with or without a trailing s and with or without a trailing Arg or Args, or one of those words alone (configPath, keyFile, inputDir, identityFileArg, path); a name OPENING with path, file, dir, directory or folder, followed by s, name or names or nothing, and then a capital or the end of the name (pathValue, fileName, filename, dirPath); and the bare words target and input, with or without a trailing s and with or without a trailing Arg or Args, which a positional or flag argument takes its name from (targets, inputArg). A name outside those three shapes is NOT held -- output, destination and source compose unnoticed -- so a sink written under such a name is marked or left raw in silence rather than being reported, and what the check holds is the shape it can recognize by name and not every operator path; a sink named outside the shapes is held by its case in apps/cli/test/unit/commands/operatorPathMarks.test.ts rather than by the ban. A value a partner or a server named is a match the ban is wrong about and takes a one-line eslint-disable, the same opt-out as its two sibling bans. scripts/eslint-unmarked-operator-path-ban.test.mjs holds the ban's own coverage -- both the names it reports and a name outside the pattern it lets through -- because a selector that stops matching keeps reporting zero problems.

Every CLI source is linted by the ban, new files included -- held for the names the three shapes above reach and for no others -- and the sensitive-file re-export shim takes a config block of its own, so the raw-parse exemption it needs does not spare it this one. No source is exempt. What stays unmarked is a path whose bytes a partner or a server chooses in part -- the operator's configured rendezvous directory joined to a peer-chosen name, or that directory seeded from the invitation on an offline accept -- and such a path keeps the escape whole, so the operator's own directory renders there with its separators doubled; over-escaping is the safe direction. Each site takes the one-line eslint-disable, stating why where it sits -- the inbound file of the frame-size refusal, the rendezvous directory of the listing refusals (seeded from a partner's invitation endpoint on an offline accept), the stalled operation's path, and the write destination an indeterminate write names (apps/cli/src/connection/frameSizeGuard.ts, listingGuard.ts, sftpLivenessGuard.ts, sftpAdapterWarnings.ts). What the ban cannot see is a path under a name none of the three shapes reaches -- output or destination above -- so a sink written under such a name is held by a case of its own or not at all. On the mark are the first-use host-key pin lines (apps/cli/src/hostKeyTrust.ts), every sink the ban names in the command sources under apps/cli/src/commands -- the configuration, key-file, signing-identity, record and receipt refusals, the line each command writes reporting what it wrote and where, and init's overwrite prompt -- the key-file and signing-identity refusals (apps/cli/src/keyFile.ts, apps/cli/src/signingIdentityFile.ts), the configuration reader's refusals (apps/cli/src/config.ts, apps/cli/src/config/loaders.ts, apps/cli/src/config/persist.ts, apps/cli/src/config/termsUpdate.ts), the permission, access-list and overwrite refusals of the owner-only writers (apps/cli/src/fileUtils.ts), the rotated-token save refusal (apps/cli/src/protocol.ts), the online bootstrap's late-conflict refusal and its post-exchange summaries (apps/cli/src/onlineBootstrap.ts), the missing-input refusal of the CSV input reader (apps/cli/src/util/dataIo.ts), and the lines the record and receipt writers report what they wrote on (apps/cli/src/recordFile.ts, apps/cli/src/receiptFile.ts). apps/cli/test/unit/commands/operatorPathMarks.test.ts and apps/cli/test/unit/operatorPathMarks.test.ts drive each sink and read the marked spans off what it produced.

The sensitive-parse chokepoint reports its failure by a caller-supplied label, so the label holds the mark rather than the failure site: parseSensitiveYaml, editSensitiveYamlDocument and parseSensitiveJson take a SensitiveFileLabel (packages/core/src/sensitiveFile.ts), either a plain string -- which keeps the escape, and is what a label naming no operator path takes -- or a message the caller composed through messageWithOperatorText, whose spans the failure keeps. A composed message interpolates into another as its own spans, which is what lets a label partitioned at one call site reach the failure with the path inside it still the operator's. What tells one is the same registered symbol the mark on an error keys, put on the message messageWithOperatorText returns and read as an own property, so the nested route is no more forgeable than the error one: a value of that shape from a parse holds no own property under that symbol -- a parsed document has string keys and nothing else -- and one inheriting the shape from a prototype fails the own-property read, so each interpolates as its own text, escaped. First-party code is what can put the symbol on a value, reaching the registered one through Symbol.for from whichever module copy it runs in, and composing the message through messageWithOperatorText is that route.

A branded type closes one chokepoint of that convention, measured rather than assumed. A string whose bytes the partner chose leaves the wire decode as PartnerOriginText (packages/core/src/utils/partnerOriginText.ts), a type not assignable to string: concatenation, template interpolation, assignment to a string field, and a join over its list form are compile errors, so no site composes one into first-party copy. Those four forms are what a composition uses, and they are the whole of what a type refuses here: TypeScript special-cases String(value) and value.toString(), which compile on any type and hand back the partner's bytes as a plain string, and no type blocks them without a lint rule Alcove does not run. An explicit conversion at a composition site is therefore the tell a review looks for rather than a build failure, and packages/core/test/utils/partnerOriginText.test.ts records both forms compiling, so the bound stated here cannot drift from what the compiler does. Its single elimination builds the Error itself -- first-party text as the message, and each partner value redacted, control-replaced, fitted to the per-value budget and labelled -- so "a labelled place per chooser" is the type's obligation rather than the composition site's. The claim it holds is scoped to that: a chooser's fragment reaches a message only behind a first-party label, never inside Error.message, which stays a plain string so classification and message-equality consumers are unchanged.

The values are packed three to a cause link rather than taking a link each, and the ceiling that follows is measured against the renderer: it walks at most eight links (MAX_ERROR_CAUSE_DEPTH), so a link per value would show seven and mark the rest elided, while three labelled values to a link -- 962 rendered characters at the label and per-value budgets, inside the per-link cap of 1024, so the renderer never clips a link this builds -- show eighteen, which is every reason site a terms refusal states itself from: seventeen in validateCompatibility and the responder's parse refusal. Past eighteen the chain ends in one first-party link counting the values it does not show, so a partner sending the wire's 256 abort reasons cannot make the loss silent. That guarantee holds while the abort error is the outermost rendered link: a full chain spends all eight links the renderer walks, so one wrapper around the abort error would push the counted tail past the bound and leave the renderer's own elision marker, which holds no count, in its place. Every path keeps it outermost -- it propagates unwrapped from the terms exchange to the display sink -- and the abort-path tests in packages/core/test/partnerAbortLinkBudget.test.ts measure that through the real terms exchange, so a wrapper added later fails there rather than costing the operator the count in silence; a wrapper a future path needs would have to budget the count one link deeper. The count is exact only because every label leads with the value's 1-based position: the renderer suppresses a cause link whose raw message repeats the previous link's, so a partner repeating one reason would otherwise fill link after byte-identical link and have all but the first dropped, uncounted, while no two positions are equal and so no two links a chain this builds are either. A line break separates the values packed on one link, and each value reaches the operator on a line of its own, opening on its first-party label; no value can open a label-led line (the browser seat's marker-led line is described below): a value's own control characters are replaced before it is packed, so a line break it holds arrives as the <0a> marker, and the link is marked with the lines it was packed from (keepFirstPartyLineBreaks), so the renderer keeps the breaks between those lines alone and escapes each line whole.

Its reach today is two chokepoints. The terms exchange's abortReasons on both abort-bearing frames takes the LIST form, and the console's retained CLI stderr tail (attachStderrTail in apps/web/src/jobs/cliDriver.ts) takes the SCALAR one: the bytes a driven child wrote are branded as they leave the read that retains them, so the terminal the job manager synthesizes for a run that emitted none can only name them through the one elimination. No wire schema takes the scalar form yet. The scalar form leads its label with no position, where the list form does: the position tells one labelled value from the next, and a scalar holds exactly one by its type. Its elimination also chooses which window of an over-budget value the operator is shown, which is what a retained output needs of a fit -- a run writes its diagnosis last, so a cut taken from the front deletes the line the link exists to carry. The window moves the clip alone: the redaction and the control replacement run over the whole value under either, and a cut that keeps the END backs off past a control-character marker it landed inside exactly as the prefix cut backs off before one (trimPartialControlCharacterMarkerAtStart).

What the console gains at that chokepoint is a boundary the child cannot spell: its stderr holds a rendered Alcove chain, framing and all, and the relay cannot tell that framing from a partner fragment inside it spelling the same bytes, so the elimination's control replacement leaves the whole tail inside the one link the console labelled. That link is laid out on the lines the child wrote: one seat helper (layOutValueLineBreaks in apps/web/src/exchange/RunSurface.tsx) breaks a line in front of each <0a> marker -- the line-break marker alone, with a carriage return, a tab and every other control marker rendered inline -- once the escape has run, and both places a seat shows a failure's text apply it: the message span, and the labelled report block holding the relayed chain where the category states copy of its own in front of it. So a diagnosis of several lines is read over several lines on either, and the marker standing at the head of each line it opens is what tells such a line from a link boundary, which opens on the separator's own caused by: text. The break is layout and nothing else -- it changes no byte of Error.message and none of the string the relay hands the seat -- and apps/web/test/browser/failureMessageLayout.test.ts measures it at the rendered line boxes of both, over a tail that spells that separator text itself. apps/web/test/unit/jobs/synthesizedTerminalProvenance.test.ts holds that over the class -- each of the two synthesized terminals, over a dangling BEGIN, a lone END, and a whole block planted in the tail -- and fails unless the console's own sentence arrives byte for byte and the child's bytes arrive on a labelled link of their own; apps/web/test/unit/jobs/stderrTailBudget.test.ts drives the same route to the string a seat renders. packages/core/test/partnerAbortLinkBudget.test.ts pins the list form, planting a dangling marker, a whole block, a forged link boundary, a forged value separator, a value ending in the escape's own token and an unbounded value in each reason of each frame, again at seven positions of a twenty-reason refusal and at the two the ceiling leaves to its count, and repeating one reason for every place the chain has; it fails unless the first-party sentence and every reason the partner did not plant in arrive whole and what the ceiling leaves out is counted.

What it does not reach among the wire schemas was measured by applying the brand to every partner-chosen string field of them and reading what stopped compiling: the terms exchange's host-key advertisement, whose divergence warning is a display string and not an error chain; the wire certificate's identity, whose schema is the type an operator's on-disk identity also takes; and the partner payload's column names and cells, which are record data rather than message text. A field its own schema pins to a lexical form with no free bytes -- the base64url shared secret -- is not branded, having nothing to plant. The file-drop directory listing is the other chokepoint outside it: a listing entry's name is a path the transport then operates on as well as text a refusal names, so the brand cannot sit on the listing itself, and both refusals that name one (filenameTooLongError in apps/cli/src/connection/listingGuard.ts and the rendezvous entry guard in packages/core/src/connection/fileSyncRendezvous.ts) hold their fragments as link STRINGS -- one of them enumerating several names on a single link under a count -- which the one elimination does not build. Those sites stay governed by the convention. The type reaches the error path only: the log prefixer in packages/core/src/utils/logger.ts, the CLI's fd-3 warning event and its plain-line sink take plain strings and remain governed by that convention.

On the abort route that layout meets the packed list, and each value keeps a line of its own. The values of a PartnerOriginTextList are packed several to a cause link, separated by one line break this composition places. The link is marked with the lines it was packed from, so the renderer keeps that break and the seat reads each value on a line of its own, opening on its 1-based position and first-party label. A line break a value holds is control-replaced before the value is packed, so the partner's copy of the separator reaches the seat as the <0a> marker, and the seat's layout opens a line in front of it. The seat opens a line only in front of the <0a> marker, which a partner can also spell literally with those four printable characters, so a marker-led line may be partner-authored. The label-led lines cannot be forged, since no value can open a line in front of a label; that is the property that matters. apps/web/test/unit/exchange/abortReasonLayout.test.ts measures the lines a packed abort reads as, including a reason that holds no break of its own and so reads as one line.

The decode description is split per consumer, which is what the assignment asks of a value that feeds both a display sink and an Error. It holds two partner-chosen fragments: the Zod issue path, which can name an object key the inviting party wrote, and the unrecognized-endpoint-key message, which echoes the rejected key names. rawDecodeErrorDescription (packages/core/src/utils/describeDecodeError.ts) composes both raw, for the CLI to interpolate into an Error that sanitizeErrorForDisplay escapes once where it is shown - the terminal render and the --event-stream error event alike, both of which take that renderer. describeDecodeError redacts and escapes the same description once and returns Displayable, for the web accept screen, where the composition IS the sink: it renders the description into a React text node, and React neutralizes HTML markup but not terminal-control, bidi-override, or zero-width bytes. Deleting the escape rather than splitting it would put partner-controlled bytes into that DOM. The redaction is the pairing every display sink takes (redactAndSanitizeForDisplay), applied there uniformly rather than on a reading of which of today's decode failures can hold a file-derived value.

Both partner-chosen fragments are also FITTED where the description is composed, rather than left to the cap at the sink. Each path segment and each rejected key name is clipped to DEFAULT_MAX_DISPLAY_LENGTH (256) by clipToRenderedCost, which measures what a fragment will render to without escaping it, so the fit is a bound on the rendered form and the single escape still happens at the sink. This is the partition by chooser, applied to a composition whose fragments sit inside one link rather than on links of their own: the path LEADS the description, so one unfitted segment -- a rejected record key, which Zod's invalid_key holds verbatim and no schema bounds in length -- spends the whole COMPOSED_MESSAGE_MAX_DISPLAY_LENGTH link and cuts the refusal reason behind it, leaving the operator a screenful of the inviting party's own bytes and no diagnosis. Each fragment is redacted before that clip and never after, since the clip appends a truncation marker that a BEGIN marker left dangling in the kept prefix would consume under the fail-closed rule. The fit is pinned at the rendered boundary in both positions, with a key past every budget: packages/core/test/config/invitation.test.ts for the web alert and the CLI composition, and apps/cli/test/unit/invitationDecodeBudget.test.ts through the real decode wrapper.

Each fragment is CUT to a raw length before either treatment, at two UTF-16 code units per character of the budget it is fitted to (boundRawFragmentForFit, packages/core/src/utils/sanitizeForDisplay.ts). The clip measures a fragment by materializing its escaped form, so measuring an uncut one costs time and memory linear in what the party that chose it sent -- and on the wire linkage-terms route the path segment is a transform.params key bounded by MAX_FRAME_SIZE_BYTES alone. A key of 5,000,000 code units measured at 755 ms and 271 MB of transient heap to describe, against 1.2 ms and no measurable growth once cut. Two code units per budget character rather than one: no code unit escapes to fewer than one output character, but a fragment cut to exactly the budget in printable ASCII renders at exactly the budget, which the clip returns whole and unmarked where the fragment it came from would have been clipped and marked. At twice the budget a cut fragment renders past the budget for a private key alone, text ahead of a private key, and plain text of any length, so for those shapes what the operator reads is what the uncut fragment would have rendered. The cut runs before the redaction, so a cut through a private-key block leaves a BEGIN marker whose END is gone and the fail-closed dangling rule takes it along with everything after it, while a cut landing inside the marker keeps no key body at all -- and a complete private-key block followed by more text falls outside the shapes above: redaction can shrink that cut fragment below the budget, and the fit then shows less of the trailing text than the uncut fragment would, with no truncation marker to say text was dropped. No key material survives either way. The redaction past a cut block is held by a check rather than by this paragraph; the fitted-text identity is checked only for the shapes named above, not for a key followed by more text (packages/core/test/utils/sanitizeForDisplay.test.ts, packages/core/test/utils/describeDecodeError.test.ts).

The Displayable brand is the compile-time half of the split. The accept screen's decode-error field is declared Displayable rather than string, so filling it from the raw form is a type error, and the raw form is a plain string so a wire echo still composes with it - the responder relays its parse description to the partner as an abort reason. Both routes are pinned at the RENDERED boundary rather than on a raw .message: the invitation route in packages/core/test/config/invitation.test.ts and, through the real decode wrapper and both CLI sinks, apps/cli/test/unit/decodeErrorEscaping.test.ts; the browser route in apps/web/test/browser/accept.test.ts, against the mounted screen; the partner linkage-terms route in packages/core/test/termsExchange.test.ts. Each asserts the rendered text against ONE escape of the partner's own bytes, so a second pass anywhere on either route fails it.

One route still escapes at composition and is escaped again at its sink, so a value crossing it makes two passes. It predates the assignment above and is recorded rather than closed, because the fix changes a published surface this section's rule does not by itself decide: what the @alcove/core return contract promises about text a consumer escapes again.

The compatibility warnings that validateCompatibility returns are handed to the caller as display text, so that composition is their sink, while each warning is escaped again where it reaches a log line, the machine-interface event stream's message, and -- through the event's unescapedMessage, which holds the composition as core returned it -- the console seat. This one is inert: every value interpolated into a warning is schema-constrained to a shape the escape does not rewrite, which is enforced by a test rather than assumed. Closing it means changing the @alcove/core return contract.

reconcileHostKeyFingerprints composes the host-key divergence notice raw, each of the four values it names -- both parties' key types and both fingerprints -- redacted and fitted to the per-value budget where it is interpolated, so the log line, the event stream's message and the console seat each escape it once: a partner fingerprint with one literal backslash shows two on every one of them.

The console adds no pass of its own to a CLI warning, and opens no route of its own. A CLI run the console conducts reaches the operator through the browser's relay, which re-validates the event stream and escapes every string field it holds (validateAndSanitizeEvent in apps/web/src/jobs/cliDriver.ts) as defense in depth at that trust-boundary crossing, save two: a warning's text, which it takes from the CLI's unescapedMessage and fits to the warning budget without escaping, and the fill warning's columns below. It then reaches the run surface's own display boundary, which escapes what it renders (appendSanitizedRunWarning in apps/web/src/psi/runWarnings.ts). So every CLI warning a console seat renders takes exactly one pass: the divergence notice's partner fingerprint with one literal backslash shows two. apps/web/test/unit/jobs/relayedWarningSingleEscape.unit.test.ts pins that per CLI warning source, from the line a child writes on fd 3 to the string a seat renders, with a value holding a backslash and a non-ASCII character.

Text the console raises itself is buffered raw and takes one seat pass alone, which is the single pass its checks pin: the rendezvous preflight's warning, naming partner-chosen directory entries, and the labelled cause link a synthesized terminal names the CLI child's retained stderr on (apps/web/test/unit/jobs/stderrTailBudget.test.ts drives that one from the child's stderr to the string a seat renders), and the notice that the CLI child could not be spawned or died abnormally, which names the child's error message fitted to one value's budget (apps/web/test/unit/jobs/relayProcessErrorSingleEscape.unit.test.ts drives it from the child's error event). The partner column names the console's notice of payload columns taken without asking is composed from take that one seat pass too: the CLI and the relay both leave them unescaped for it (SERVER_JOB_API.md), and apps/web/test/unit/jobs/payloadReceiveTakenNotice.unit.test.ts drives them from the CLI's event to the string a seat renders. That link takes one pass and not two for a further reason: the tail is branded partner-origin text, so the elimination that composes the link REPLACES its control characters where it is composed rather than leaving them for the seat to escape, and the child's own caused by framing therefore opens no link of the console's chain. That raw composition rests on every seat folding through the one boundary, so the routing is held by a lint rule rather than by convention (the seat warning-sink ban in apps/web/eslint.config.js, covered by scripts/eslint-seat-warning-sink-ban.test.mjs): a seat that escaped twice, or not at all, is a failure no test of the boundary itself can see. What that rule reads is the call shape and not scope, so a handler passed by bare shorthand -- the same spelling a seat uses to forward the slot its own caller owns -- is the one seat shape it leaves to review. A run surface cannot tell which route handed it a message, so the single pass is held where the routes meet: the relay hands the seat every warning unescaped, as the console's own notices are. Where a child sends no string unescapedMessage -- a fill warning, whose message the console replaces, or a child outside the CLI's shape -- the relay escapes the child's message again instead; over-escaping is safety-preserving, so what that costs is display fidelity rather than containment.

The cap those boundaries apply is not the per-value default, because neither boundary has a value. The relay knows which source handed it each string, so it fits a warning event's own text -- the composition the CLI fitted to WARNING_MESSAGE_MAX_DISPLAY_LENGTH (packages/core/src/utils/sanitizeForDisplay.ts, the one constant every boundary that holds a whole warning reads) -- to that same budget, while every other field it relays keeps the per-value default. The seat needs no such knowledge: each message it folds is a whole warning whichever route raised it, so the composition budget is the cap there too. The console's own preflight notices ride that same seat boundary and are uniform under it: every one of the eight fits itself at composition to DEFAULT_MAX_DISPLAY_LENGTH, so its own cut lands first and the seat's cap is slack above it. What gives way differs by what each notice interpolates. The entry listing drops a partner-chosen name that does not fit whole, counting it instead. The other six -- the non-empty lead, the missing, non-directory, non-writable, and unlistable notices, and the overlap notice, which also names the data root or work-input directory it overlaps -- interpolate the operator-configured rendezvous path, and the unresolved-real-path notice interpolates whichever directory it could not resolve (the rendezvous path, the data root, or the work-input directory); each drops its path whole when the sentence around it will not fit beside it. A clipped path reads like a path the operator could go and look at, while the sentence is the part they cannot reconstruct. What is on display there is the operator's own server-side configuration, escaped by the seat's single pass exactly as every other message the console raises is. The whole set is pinned at the rendered boundary by apps/web/test/unit/jobs/jobRendezvous.unit.test.ts, which drives each notice's own branch -- on each leg label a split console composes it for, the inbound leg's non-writable copy included -- at an ordinary mount and at one whose path is past the budget by its length and again by its escape expansion. It fails unless the clause the operator acts on survives the seat and every notice the branch raised measures inside the budget. A console seat is therefore a whole-message delivery route up to that budget, which the divergence notice fits with all four of its fragments flooded -- pinned end to end, from a child's fd 3 to the string a seat renders, by apps/web/test/unit/jobs/relayWarningBudget.test.ts. What the budget does not cover is a notice composed near it on a route that escapes it more than once -- a supervisor re-escaping the event stream's escaped message (CLI_EVENTS.md) -- where each further pass expands what it escapes, so a message close to the budget at one boundary is cut at the next.

Compatibility diagnostic value delimiting ​

The escape above governs which bytes an operator can be shown. A second, orthogonal control governs which clauses: the cross-party compatibility diagnostics (validateCompatibility, packages/core/src/linkageTermsNegotiation.ts) are first-party prose whose structure an operator treats as Alcove's own -- legal agreement reference mismatch: yours is X, your partner's is Y -- around values a mutually-distrusting partner chooses. Both sides of that sentence can be the partner's, since deriveAcceptedLinkageTerms adopts the inviter's legalAgreement verbatim into the acceptor's terms ("yours" names provenance in the exchange, not trust in the bytes). The escape does not reach this class: it rewrites what is not printable ASCII, and a reference of A", your partner's is "B or a payload column named a,b is printable ASCII throughout. What answers it is delimiting, applied once at composition.

Every value one of these diagnostics names passes through one module (packages/core/src/config/compatibilityMessage.ts) in one of two forms. quoteTermsValue wraps the value in " and doubles every " inside it: reading from the opening delimiter, a doubled delimiter is one literal character and a single one closes the run, so the encoding is injective and no value can terminate its own run early or spell a clause of its own. bareTermsValue renders undelimited the values the terms schema constrains to a shape no clause boundary is made of -- a semver string or an ISO date -- and RE-CHECKS that shape on the value in hand: the charset BARE_TERMS_VALUE_PATTERN matches letters, digits, ., _, and -, at least one of them a digit. Since that pattern bounds no length, a MAX_BARE_TERMS_VALUE_LENGTH of 64 characters bareTermsValue checks beside it. It falls back to the quoted form for anything else. The re-check is what makes it a second safe constructor rather than an override: validateCompatibility takes two LinkageTerms objects and nothing in its signature makes them schema-parsed, so the assumption is executed rather than asserted. The shape excludes the delimiter and the ,/[/] the payload list is built from; the digit is what stops a bare value spelling a connective. Excluding the space makes a bare value exactly one whitespace-delimited token, which on its own does not keep it out of a connective position, because the template supplies the spaces around a bare slot -- the version clause renders yours is <version>, your partner's is <version>, so a letters-only version would stand in that sentence as undelimited as the is beside it. Every value these diagnostics render bare has a digit and no connective or label their templates are built from does, which leaves the two vocabularies in disjoint shapes; the digit-free half of that is executed rather than asserted, packages/core/test/config/compatibilityMessage.test.ts reading the templates out of their own module and failing if any token of first-party copy meets the bare shape. A value with no digit -- an algorithm or strategy enum member, say -- takes the quoted form instead, which costs the reading nothing. A payload column list quotes per element (quoteTermsValueList), so the rendered ["a","b"] shows the same partition the byte-exact element-wise comparison used to decide the mismatch, rather than the [a,b] a single column named a,b would also print.

Doubling rather than a backslash escape is what keeps the control orthogonal to the escape at the sink: the module adds only printable ASCII, which sanitizeForDisplay passes through unchanged, so a run's boundaries survive the display boundary byte for byte and neither transform doubles the other's work. On the warnings route, which escapes at composition, the escape runs before the delimiting, so that transform's truncation can never take the closing delimiter off a run.

Beside the delimiting, quoteTermsValue replaces every control character in the value (Unicode Cc, so U+0000-U+001F and U+007F-U+009F) with a printable marker naming its code point in two lowercase hex digits inside angle brackets -- <0a> for a line break, the same four characters wide the escape's own \x0a is. So a budget measured over a treated fragment is the budget the untreated one asked for (replaceControlCharactersForDisplay, packages/core/src/utils/sanitizeForDisplay.ts). Delimiting bounds what a value can do in the message's printable structure; this bounds what it can do in the bytes a composition builds structure out of one level up. A message whose own structure is a control character -- a block separating its lines with \n -- states those lines to the display boundary through keepFirstPartyLineBreaks (packages/core/src/utils/sanitizeErrorForDisplay.ts), which escapes each line and joins them with the break, so the operator reads the block as a block. This treatment is what keeps a value's own line break out of that reading: inside a line it is the <0a> marker, not a break and not the escape's \x0a either. The marker has no backslash and nothing outside printable ASCII, so the sink's single pass finds nothing to rewrite: this is a composition-site treatment, the same shape as the private-key redaction, not a second escaping altitude. On the warnings route the escape again runs ahead of the module, so nothing is left there for the treatment to act on and a value's control character instead arrives one backslash wider than a composition's own would -- the observable doubling that route already has, standing as the same distinction. Being printable, the marker is not authenticated and cannot be: a value that spells it renders as one that held the character it names, the same open class DISPLAY_TRUNCATION_MARKER has; what an operator relies on is the converse, that a control character Alcove itself composed renders as the escape's \xHH -- or, for the line breaks a composition states as its own, as a line break -- and no value can produce either. packages/core/test/utils/sanitizeForDisplay.test.ts drives the whole control class through the marker and fails if any of it renders as a token the escape writes for a control character, or if a marker costs the display more than the character it replaced; packages/core/test/config/compatibilityMessage.test.ts and apps/cli/test/unit/config.test.ts assert the property where each renderer shows it.

A kept break is also the one byte a first-party composition puts in front of the cause chain's own framing, so keepFirstPartyLineBreaks refuses a line that would stand behind it as a link boundary. A line opening on the separator's caused by: text is prefixed with a backslash, which the escape doubles: the operator reads \\caused by: , and the split sanitizeErrorChainLinks performs reads no boundary there. It is enforced at the mark, over every line the caller hands over, rather than asked of the caller -- the parameter is a plain string array, so a later composition could put a value somebody else chose at the start of a line. The renderer applies it again to the lines it splits out of the marked text, since the mark holds that text rather than the lines it was stated as: code in the process that writes the registered symbol itself forges no boundary either (packages/core/test/utils/sanitizeErrorForDisplay.test.ts).

The enumeration is held by type rather than by a list. validateCompatibility accumulates CompatibilityMessageFragment -- a string intersected with a phantom property keyed by a unique symbol private to that module, the same brand construction the invitation consent summary uses for Displayable -- and the only ways to obtain one are the two constructors above and the compatibilityMessage tagged template, whose fixed spans only the compiler produces. A terms value interpolated into a diagnostic without passing the module does not compile, for a message edited later as much as for a mismatch check added later. packages/core/test/config/compatibilityMessage.test.ts is the runtime half: it drives every value slot of every diagnostic that names one -- both sides of a two-slot comparison -- twice, once with a value it legitimately holds and once with an adversarial shape built around that same value. These are: one with the delimiter, one with every clause connective these messages are built from, one reading as a name followed by a version, and two the bare charset admits, a connective spelled outright and a digit-carrying value that stays bare. It asserts the two runs produce the identical clause skeleton and that the value is kept whole, inside one run or, where the checked bare shape let it through undelimited, verbatim in the clause. The last two are what reach the bare branch at all, the first three each with a space or a comma that delimits them whatever slot they land in. Both then have to hold again on what the diagnostic's own route renders, which is not one pipeline: an errors diagnostic composes raw and is asserted through the Error its caller throws and the single escape at the error renderer, while a warnings one is asserted through the escape it already took at composition and the further pass its sink applies. The pass count is asserted rather than assumed, and asserting it takes the negative: a second pass doubles the first pass's backslash, so the once-escaped form is a substring of the twice-escaped one and looking for it alone cannot tell them apart. The single-pass route is pinned by the absence of the doubled form and the warnings route by its presence -- reachable there because validateCompatibility takes two LinkageTerms objects its signature does not make schema-parsed, which is the same assumption bareTermsValue re-checks; a date that arrived through the terms schema has no byte either pass rewrites, which is what keeps that route inert above.

The same module composes the payload-disclosure refusal (assertPayloadSendDisclosed, packages/core/src/payloadExchange.ts). That one is not a cross-party comparison -- it holds this party's own payload.send dictionary to what its metadata actually transmits -- but it names the same class of value, because on the accept side both the dictionary and the terms around it are the inviter's, adopted by deriveAcceptedLinkageTerms. Its problem and remedy clauses accumulate CompatibilityMessageFragment and its name lists render through quoteTermsValueList, so the bracketed list an operator reads shows the same element partition the membership comparison used, and no column name can spell a clause of the refusal's own. packages/core/test/payloadExchange.test.ts drives each of the three lists that message renders -- the over-declared list, the under-declared list, and the under-declared list under an empty send, whose remedy is prose of its own -- with a delimiter-carrying name, a name spelling the message's own clause separators, and a name forging an extra element into the list. It asserts each produces the clause skeleton the benign run did: on the composed message, and again through the single escape at the error renderer that delivers the UsageError to the operator. The reader those assertions invert is the one the module's own suite pins against the constructor (packages/core/test/utils/compatibilityMessageReader.ts), shared rather than restated so the two suites cannot drift apart on what a run is.

One CLI message, composed in apps/cli/src/config/reconcileDiffs.ts, is not a cross-party comparison but names the same class of value in the same kind of first-party clause. The accept-time reconcile refusal renders one line per disagreeing field -- linkage_keys: existing X vs required Y -- and both of its sides can be the partner's, since deriveAcceptedLinkageTerms puts the inviter's legal agreement into the terms an earlier acceptance wrote to the kept configuration, and an online accept's connection locators come from that party's endpoint. The enumeration is held by type there as it is in core: ReconcileDiff's two sides are declared CompatibilityMessageFragment rather than string, so a producer -- in apps/cli/src/config/reconcileDiffs.ts or in apps/cli/src/reconcile.ts -- that interpolates a raw value into a conflict line does not compile. Where a name-only rendering cannot tell two structures apart, the full-JSON fallback it falls back to is delimited as one run over the serialized text, which reaches a name nested anywhere inside it. The legal agreement clause renders the reference and the expiration date, and the purpose as well where the two agreements differ in it, so every field the conflict compares is one the clause can show; the agreement conflict has no full-detail fallback of its own, so two clauses that read alike are two agreements whose difference the redaction or the fit took out. The message falls under the three limits recorded below, except that it closes the first two -- by the budget partition and the fit stated with that refusal, not here: its conflict block is fitted at composition, so a cut lands inside a run the fit closes rather than leaving one open. Each of its values is redacted where it is interpolated with the recovery step composed ahead of the block, so a planted PEM BEGIN marker takes neither a run's closing delimiter nor that step.

Three limits are recorded rather than closed -- all three for every message this module composes, but for the accept-time reconcile refusal only the last, as its paragraph states. The per-link display cap can cut an over-long diagnostic mid-run -- a single name at the payload schema's 256-character bound is enough to push the refusal past it -- which leaves an unterminated run followed by DISPLAY_TRUNCATION_MARKER, readable as cut, not as a further clause, at the cost of whatever the cap cut behind the cut, the remedy sentence included. A partner value with a PEM BEGIN armor line trips the error renderer's fail-closed private-key redaction, whose replacement consumes from the marker onward. The run's closing delimiter and the clauses composed behind it -- again the remedy -- are lost, though the replacement is fixed first-party ASCII, so what a planted marker costs is the operator's recovery guidance, never a forged clause. And delimiting is not escaping: the bytes inside a run are the raw partner value but for the control characters the treatment above replaces, which is what the single-altitude assignment intends. So what the brand guarantees is that a value can neither leave its run nor render as a control character the composition placed -- not that its content is safe to render by some other means. A confusable, a bidi override, and a zero-width character stay in the run raw, for the single escape at the sink to render visibly.