Fountain shipped the fix this workaround compensates for, so apps/salon/server/sandbox.ts#unescaped can go.
Background
Salon declares its conversation-authenticated MCP server with Fountain's $$ escape (server/agents.ts:82):
headers: { Authorization: "Bearer $${FOUNTAIN_TOKEN}", "X-Fountain-Conversation-Id": "$${FOUNTAIN_CONVERSATION_ID}" }
Fountain wrote that config into the computer twice: substituted into the project .mcp.json ($${X} → ${X}, which the runtime then expands from its own environment), and raw on the ACP session. The session copy is the one Claude Code uses, so the runtime expanded the inner ${X} and left the leading $ standing, and the header arrived as Bearer $ftn_…. Salon dropped that $ on the way in.
BinaryBourbon/fountain#1404 fixed it: there is one substitution pass and one effective configuration now, and session/new gets the resolved document. The header arrives as Bearer ftn_….
What to change
- Delete
unescaped() — server/sandbox.ts:68.
- Drop its two call sites —
server/sandbox.ts:48 (the bearer key) and :50 (the conversation id). Both become the plain trimmed value.
- Update
apps/salon/CLAUDE.md — the workaround is described at ~109-127 and again at ~374 ("fix there, then delete sandbox.ts#unescaped"). Note that line 127 says mcp.ts#unescaped; there is no unescaped in mcp.ts, so that reference is stale either way.
Do NOT remove the $$ escapes
server/agents.ts:82 should keep "Bearer $${FOUNTAIN_TOKEN}" exactly as it is. That is the correct pattern and the fix depends on it: Fountain's single pass turns $$ into $, leaving a literal ${FOUNTAIN_TOKEN} for the runtime to expand from the sandbox's process environment — which is what keeps a reattached or resumed turn on the current credential instead of one frozen into the stored agent document.
Deleting the escape as well would send an unresolved ${FOUNTAIN_TOKEN} that Fountain has no value for, or an eagerly-resolved token that goes stale. The workaround is wrong now; the escape is not.
Version requirement
The fix is live on the hosted instance (merged as 012d7ac5, deployed in sha-dc8bd31a, 2026-09-03), so this is safe against fountain.inevitable.fyi today.
It is not in a tagged release — v0.16.0 predates it. If any Salon deployment ever points at a pinned self-hosted Fountain at v0.16.0 or earlier, removing unescaped() breaks it until that instance takes the fix. Worth a glance at fountainUrl before merging.
Verifying
The unescaped() removal is safe on its own reading: a Fountain API key is ftn_… and a conversation id is a UUID, so neither legitimately begins with $. The helper only ever compensated for the bug.
Salon is the only app in this repo carrying it — grepped apps/ for unescaped and $${, and the other hits are unrelated (fountain-demos/src/roster.test.ts is about HTML quoting).
Refs: BinaryBourbon/fountain#1404, PR BinaryBourbon/fountain#1511.
Fountain shipped the fix this workaround compensates for, so
apps/salon/server/sandbox.ts#unescapedcan go.Background
Salon declares its conversation-authenticated MCP server with Fountain's
$$escape (server/agents.ts:82):Fountain wrote that config into the computer twice: substituted into the project
.mcp.json($${X}→${X}, which the runtime then expands from its own environment), and raw on the ACP session. The session copy is the one Claude Code uses, so the runtime expanded the inner${X}and left the leading$standing, and the header arrived asBearer $ftn_…. Salon dropped that$on the way in.BinaryBourbon/fountain#1404 fixed it: there is one substitution pass and one effective configuration now, and
session/newgets the resolved document. The header arrives asBearer ftn_….What to change
unescaped()—server/sandbox.ts:68.server/sandbox.ts:48(the bearer key) and:50(the conversation id). Both become the plain trimmed value.apps/salon/CLAUDE.md— the workaround is described at ~109-127 and again at ~374 ("fix there, then deletesandbox.ts#unescaped"). Note that line 127 saysmcp.ts#unescaped; there is nounescapedinmcp.ts, so that reference is stale either way.Do NOT remove the
$$escapesserver/agents.ts:82should keep"Bearer $${FOUNTAIN_TOKEN}"exactly as it is. That is the correct pattern and the fix depends on it: Fountain's single pass turns$$into$, leaving a literal${FOUNTAIN_TOKEN}for the runtime to expand from the sandbox's process environment — which is what keeps a reattached or resumed turn on the current credential instead of one frozen into the stored agent document.Deleting the escape as well would send an unresolved
${FOUNTAIN_TOKEN}that Fountain has no value for, or an eagerly-resolved token that goes stale. The workaround is wrong now; the escape is not.Version requirement
The fix is live on the hosted instance (merged as
012d7ac5, deployed insha-dc8bd31a, 2026-09-03), so this is safe againstfountain.inevitable.fyitoday.It is not in a tagged release — v0.16.0 predates it. If any Salon deployment ever points at a pinned self-hosted Fountain at v0.16.0 or earlier, removing
unescaped()breaks it until that instance takes the fix. Worth a glance atfountainUrlbefore merging.Verifying
The
unescaped()removal is safe on its own reading: a Fountain API key isftn_…and a conversation id is a UUID, so neither legitimately begins with$. The helper only ever compensated for the bug.Salon is the only app in this repo carrying it — grepped
apps/forunescapedand$${, and the other hits are unrelated (fountain-demos/src/roster.test.tsis about HTML quoting).Refs: BinaryBourbon/fountain#1404, PR BinaryBourbon/fountain#1511.