You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Feature]: Kickoff prompt — one user-owned project-planning preamble that prefills the composer visibly, is editable in Settings, and that models can propose edits to under an explicit opt-in #46
I searched existing issues and did not find a duplicate.
I am describing a concrete problem or use case, not just a vague idea.
Area
apps/web
Problem or use case
Starting a new project is the one moment T3 Code has the least reuse to offer, and the workarounds that exist are each scoped to something that makes them not-quite-work.
I keep re-typing (or re-pasting) the same "help me plan this app idea" preamble at the start of every new thread. What I want is one prompt I own, that shows up at the new-project moment, that I can read and edit in Settings from any client, and that gets better over time because the model that just used it can suggest an improvement.
T3 already ships three overlapping pieces of this, and none of them is the whole thing. Being precise about what already works matters, because the honest version of this request is narrow.
What already works (do not rebuild these):
Claude custom slash commands — zero code, today.ClaudeAdapter sets the claude_code preset and all three setting sources (apps/server/src/provider/Layers/ClaudeAdapter.ts:3528-3529, with CLAUDE_SETTING_SOURCES at :884), so the CLI inherits ~/.claude/commands/*.md, CLAUDE.md, skills, agents and hooks. ClaudeProvider then harvests init.commands into the composer's / menu (apps/server/src/provider/Layers/ClaudeProvider.ts:747), consumed at apps/web/src/components/chat/ChatComposer.tsx:1099-1110. Selecting one inserts the literal /<name> (ChatComposer.tsx:1728) and the CLI expands it. So ~/.claude/commands/kickoff.md genuinely is a working, autocompleted kickoff prompt — I already have one such file (~/.claude/commands/animate.md), so I know the shape works.
A Settings-editable prompt precedent.sourceControlWritingStyle.customInstructions is a Settings textarea whose text is fed to a model (packages/contracts/src/settings.ts:421-429, UI apps/web/src/components/settings/SourceControlWritingSettings.tsx:118-136, consumed apps/server/src/git/GitManager.ts:612-615).
A reusable-prompt-into-composer surface. The prompt stash: Cmd+S, 20 entries, t3code:prompt-stash:v2 (apps/web/src/promptStashStore.ts:7,:17).
The four gaps those leave, each with evidence:
Cross-provider. Slash commands are Claude-only. command grep -rn "slashCommands" apps/server/src/provider/Layers/ returns producers only in ClaudeProvider.ts (:747, :904-935). CodexProvider surfaces skills (client.request("skills/list") at CodexProvider.ts:394) but never slashCommands; CursorProvider/GrokProvider/OpenCodeProvider have neither. A Codex, Cursor, Grok or OpenCode user has no equivalent at all.
Remote-client reach.~/.claude/commands is a directory on the host filesystem. A phone or a browser paired to a remote T3 server can neither see nor edit it.
Transcript honesty. T3 sends /kickoff … verbatim and the CLI expands it out of band, so neither T3 nor I ever see what was actually sent.
Model-editable. Nothing anywhere lets a model revise a prompt I own. This one has zero prior art (see Duplicate search).
Plus a fifth, softer one: discoverability. A kickoff prompt only helps if it is present when I start a new project. A slash command I have to remember to type is not.
Proposed solution
The shape: a "kickoff prompt"
One named, fork-owned, server-stored preamble. Three surfaces: a chip at the new-project moment, an editor in Settings, and (phase 2) two MCP tools so the model can propose a revision.
The central UX call — committed: visible prefill, triggered by a chip. Not an invisible prepend.
Three candidates were on the table:
(a) prefill as visible, editable text in the composer
(b) invisibly prepend on send
(c) a one-tap chip at the new-thread empty state
Ship (c) as the trigger and (a) as the behaviour: a chip under the draft hero that prefills the composer with visible, editable text. Reject (b) outright.
Evidence for that call:
(b) is architecturally wrong here. The client assembles the final message entirely client-side inside onSend and sends it as thread.turn.start (packages/client-runtime/src/operations/commands.ts:232-242); the same string builds the optimistic user bubble (apps/web/src/components/ChatView.tsx:4886, :4929, :5057). A server-side prepend makes the rendered transcript disagree with what the model received. A client-side prepend means editing ChatView.tsx — the fork's worst seam at risk 11466.
(a) is nearly free. The composer's text is not local state: it is read from a zustand store (apps/web/src/components/chat/ChatComposer.tsx:707useComposerThreadDraft(composerDraftTarget), :708, :716store.setPrompt), and the Lexical editor is fully controlled — a layout effect rewrites editor state whenever the external value differs (apps/web/src/components/ComposerPromptEditor.tsx:1605-1610, $setComposerEditorPrompt(value, …)). Upstream already calls the store imperatively from outside React (apps/web/src/composerDraftStore.ts:3541). Prefill is one line and requires zero composer edits.
(b) is the thing that already exists badly. An always-on invisible preamble is what --append-system-prompt in Claude launch args would give you (see Alternatives) — riding along on every message, invisible, undebuggable.
The chip lands at the right moment.isDraftHeroState (ChatView.tsx:2421) renders <DraftHeroHeadline> — "What should we build in {project}?" — directly above the composer (ChatView.tsx:6058). That is literally the new-project moment.
Explicitly not v1: auto-arming on every new draft. Composer drafts persist to localStorage under t3code:composer-drafts:v1 (composerDraftStore.ts:59), so auto-prefilled text is indistinguishable from text I typed, and I would have to delete a wall of text every time I just want to ask a quick question. Revisit as an opt-in toggle later.
Storage — fork-owned, never packages/contracts/src/settings.ts
Copy apps/server/src/t3x/autoResume/state.ts line for line: a single JSON file in the server state dir, an Effect Schema.Struct, mutations serialised through a SynchronizedRef and persisted atomically via apps/server/src/atomicWrite.ts.
{version: 1,text: string,// the live promptupdatedAtMs: number,modelEditingEnabled: boolean,// DEFAULT falsependingProposal: null|{ text, rationale, proposedAtMs, threadId, providerInstanceId },history: Array<{text,savedAtMs,source: "default"|"user"|"model-accepted"}>,// cap 20}
Every new field must decode with Schema.withDecodingDefaultKey — autoResume/state.ts:44-53 documents why: a missing required key fails the whole-file decode and the boot path silently drops everything.
Wire it in apps/server/src/t3x/index.ts exactly like AutoResumeStoreLive (Layer.effect reading ServerConfig.stateDir + Path.Path). Zero new upstream rows.
HTTP — /api/t3x/kickoff-prompt
apps/server/src/t3x/kickoffPrompt/http.ts, modelled on apps/server/src/t3x/autoResume/http.ts:
GET → { text, updatedAtMs, modelEditingEnabled, pendingProposal, history }
POST → { text?, modelEditingEnabled?, acceptProposal?, rejectProposal?, revertToVersionAtMs? }
Reuse the authenticateWithOperateScope mirror (autoResume/http.ts:38-57) — it is already a documented logic mirror in docs/t3x/SEAMS.md. Register through T3xRoutesLive. Zero new upstream rows. A raw route rather than a WS-RPC method, for the reason autoResume/http.ts:5-10 gives: an RPC would force edits to @t3tools/contracts plus apps/server/src/ws.ts (churn 41).
Web — chip
apps/web/src/t3x/KickoffPromptChip.tsx (fork-owned). Mounted in apps/web/src/components/chat/DraftHeroHeadline.tsx, which today returns a bare <h1> (:147-157) — wrap it in a fragment and append the chip. That component already receives activeProjectRef (:27-35), so no ChatView.tsx edit is needed.
Resolving the composer target: prefer the route param via TanStack Router (useParams({ from: "/_chat/draft/$draftId", shouldThrow: false })) read inside the fork component; fall back to useComposerDraftStore.getState().getDraftThreadByProjectRef(activeProjectRef)?.draftId (composerDraftStore.ts:338, :2213; ProjectDraftSession extends DraftSessionState { draftId: DraftId } at :306-308). ComposerThreadTarget = ScopedThreadRef | DraftId (:318) is not exported, but a bare DraftId is assignable, so no upstream export is needed.
Do not clobber typed text.setPrompt replaces. If getComposerDraft(target)?.prompt is non-empty, prepend kickoffText + "\n\n" instead of replacing.
Idempotent. If the current prompt already starts with the kickoff text, do nothing (or no-op the chip into a "Kickoff prompt added" state).
The chip needs pointer-events-auto to match its siblings — the hero sits inside an absolute inset-x-0 bottom-full block (ChatView.tsx:6047) and the interactive elements inside DraftHeroHeadline all carry that class (:98, :141).
Cheaper alternative mount if the hero edit is rejected: apps/web/src/routes/_chat.draft.$draftId.tsx (churn 2) already holds draftId and renders <ChatView>, following the proven AutoResumeOverlay sibling pattern (apps/web/src/routes/_chat.$environmentId.$threadId.tsx:18, :92). Tradeoff: absolute positioning instead of natural in-flow placement under the headline.
Web — Settings editor
apps/web/src/t3x/KickoffPromptSettings.tsx, exported as a bare <SettingsSection>, never its own <SettingsPageContainer> — that container is flex-1 overflow-y-auto (apps/web/src/components/settings/settingsLayout.tsx:199-222), so two siblings on one route produce two competing scroll panes.
Mount inside BetaSettingsPanel's existing container (apps/web/src/components/settings/BetaSettingsPanel.tsx, which already renders <SettingsPageContainer><SettingsSection>). Churn 3 — the cheapest settings surface in the tree. A brand-new settings page would cost ~10 churn instead (exhaustive Record<SettingsPath, …> entries in both settingsSearch.ts and SettingsSidebarNav.tsx, churn 9).
Contents:
The prompt textarea — copy SourceControlWritingSettings.tsx:118-136 verbatim: uncontrolled <Textarea key={text} defaultValue={text} onBlur={commit} rows={…} aria-label> nested inside a <SettingsRow>. apps/web/src/components/ui/textarea.tsx is churn 0.
<SettingResetButton> (settingsLayout.tsx:175) → reset to the shipped default.
Switch: "Let models propose edits to this prompt" — default OFF.
Pending-proposal card: old-vs-new diff, the model's rationale, which thread proposed it, Accept / Reject.
History list with one-click restore.
Phase 2 — the model-editable half, as a fork-owned MCP toolkit
This is the half with no prior art anywhere, and it is provider-agnostic for free: all five adapters already mount a per-thread t3-code HTTP MCP server — ClaudeAdapter.ts:3552, CursorAdapter.ts:547, GrokAdapter.ts:585, OpenCodeAdapter.ts:1221, and Codex via mcp_servers.t3-code.url=… (asserted in CodexAdapter.test.ts:579). The toolkit directory today contains only preview/.
apps/server/src/t3x/kickoffPrompt/mcp/{tools,handlers}.ts, following the Tool.make(...) / Toolkit.make(...) / Toolkit.toLayer(handlers) shape in apps/server/src/mcp/toolkits/preview/:
t3x_kickoff_prompt_get — returns the current text. Tool.Readonly true.
t3x_kickoff_prompt_propose — { text, rationale }. Writes pendingProposal. Returns a message saying it is queued for the user's review and is not live.
There is deliberately no tool that mutates the live prompt. The worst a prompt-injected or compromised model can do is queue a proposal I have to read and accept. That is the whole permission story, and it is stronger than anything an approval dialog could give.
The opt-in gate must be enforced inside the fork's handler, not delegated to T3's approval system. Two verified reasons:
apps/server/src/provider/Layers/ClaudeAdapter.ts:3372-3378 — runtimeMode === "full-access" returns { behavior: "allow" }before any ApprovalRequestId is minted. Most threads run in full-access.
So: read modelEditingEnabled from the store inside the handler and fail with a typed error the model can read and explain — the same shape as requireMcpCapability (apps/server/src/mcp/McpInvocationContext.ts:26-40), but keyed on a persisted setting rather than the issued capability set. Do not widen McpCapability (McpInvocationContext.ts:10, churn 3) or the hardcoded issued set (McpSessionRegistry.ts:131, churn 7) — that would cost two extra ledger rows.
Caller identity comes free and cannot be spoofed: McpInvocationContext carries environmentId/threadId/providerSessionId/providerInstanceId (McpInvocationContext.ts:12-19), bound to the request by bearer token in the auth middleware (McpHttpServer.ts:70-89). It does not carry projectId.
Mount point (UNVERIFIED — see Risks).McpServer.layerHttpoutputs the McpServer service, and McpHttpServer.layer = PreviewToolkitRegistrationLive.pipe(Layer.provideMerge(McpTransportLive)) (apps/server/src/mcp/McpHttpServer.ts:206-225), so McpServer is in that layer's public output. Therefore apps/server/src/server.ts:427:
server.ts is already a ledger row (+3 lines, churn 29, risk 87) — this grows it rather than adding a row, and keeps apps/server/src/mcp/* untouched.
Tool discoverability across providers. The only place T3 tells a model the t3-code server exists is apps/server/src/provider/CodexDeveloperInstructions.ts:7, and it is Codex-only. Rather than add per-adapter hints, let the kickoff prompt advertise its own tools: when modelEditingEnabled is on, append a line to the resolved text. That is fork-owned data, zero seam, and works on all five providers:
If you learn something in this session that would make this kickoff prompt work better next time, call t3x_kickoff_prompt_propose with the full revised text and a one-line rationale. It queues a proposal for my review; it does not change anything.
Default prompt
Ship a working default so the feature needs zero setup (autoResume's DEFAULT_RESUME_PROMPT = "continue" at config.ts:106 is the precedent). Something like:
You are helping me plan a new project from scratch. Before writing any code:
1. Ask clarifying questions, a few at a time, until you can state the problem, the
user, and the smallest shippable version in one paragraph each.
2. Propose 2-3 candidate approaches with real tradeoffs, and recommend one.
3. Name the stack and justify each choice against my constraints, not against fashion.
4. Sketch the smallest thing that could ship this week, and what it deliberately omits.
5. Stop and wait for my go-ahead before scaffolding anything.
Here is what I want to build:
Why this matters
Who benefits, concretely:
Non-Claude users get this at all. Today a Codex, Cursor, Grok or OpenCode user has no reusable-prompt mechanism in T3's composer — only ClaudeProvider populates slashCommands (ClaudeProvider.ts:747). This works identically on all five providers because it is plain text written into upstream's own composer store.
Remote and mobile clients get it.~/.claude/commands/*.md lives on the host filesystem and is unreachable from a browser or phone paired to a remote T3 server. /api/t3x/kickoff-prompt is reachable from any paired client, over the same primaryEnvironmentHttpLayer path AutoResumeOverlay already uses.
The new-project moment becomes a single tap. The chip sits under "What should we build in {project}?" — the exact instant the prompt is useful. That is the difference between a feature I use and a slash command I forget exists.
What I send is what the model gets. The preamble is in the composer, in my editor, before I hit send — and identical to what lands in the transcript. The /kickoff path can never offer that, because the CLI expands the command out of band and T3 never sees the expansion.
The prompt gets better without me maintaining it. After a planning session the model has just learned where my preamble was vague. Today that knowledge evaporates. A propose-only tool captures it as a reviewable diff.
What it unlocks for the fork specifically: this is the first fork feature to add a t3-code MCP tool. Once T3xMcpToolkitsLive exists and compiles, issues #42 (wake_me) and #43 (provider-agnostic orchestration) inherit the mount for free — they are both blocked on exactly this plumbing.
Smallest useful scope
Phase 1 — no MCP, no model editing, no history. Ship this alone; it fixes four of the five gaps.
apps/server/src/t3x/kickoffPrompt/state.ts — JSON store at <stateDir>/t3x-kickoff-prompt.json, { version: 1, text, updatedAtMs } only. Copy autoResume/state.ts.
apps/server/src/t3x/kickoffPrompt/http.ts — GET/POST/api/t3x/kickoff-prompt, operate scope, registered through T3xRoutesLive in apps/server/src/t3x/index.ts.
apps/web/src/t3x/KickoffPromptChip.tsx + a ~5-line mount in DraftHeroHeadline.tsx (wrap the <h1> at :147-157 in a fragment).
apps/web/src/t3x/KickoffPromptSettings.tsx (bare <SettingsSection>: textarea + reset) + a ~3-line mount inside BetaSettingsPanel.tsx's existing container.
Upstream cost of Phase 1: two new ledger rows, ~8 changed lines, combined risk ≈ 34. Zero server rows, zero contracts rows, zero ChatView.tsx, zero ChatComposer.tsx.
(Churn re-measured against merge-base 64bf01619 with the docs/t3x/SEAMS.md recipe on 2026-08-02.)
Phase 2 — the model-editable half.modelEditingEnabled switch (default OFF), the two MCP tools, pendingProposal + accept/reject diff card, history + one-click revert, and the tool-advertising suffix on the resolved prompt. Adds ~2 lines to the existing apps/server/src/server.ts row (risk +~58).
Deliberately deferred, in priority order:
Mobile parity. apps/mobile/src/features/threads/ThreadComposer.tsx is churn 16 / risk 480 and already a fork seam. Web-only for v1.
Registration in upstream's settings-search catalog (apps/web/src/components/settings/settingsSearch.ts, churn 1). Near-free in risk terms but it is a third new ledger row against an explicit tripwire; the fork's existing settings row already made this same call and is unfindable via search. Judgement call, revisit.
A per-project override in t3.json (packages/contracts/src/t3ProjectFile.ts:61-86 — it has $schema, iconPath, scripts, no prompt field). Also note McpInvocationScope carries threadId but notprojectId, so a project-scoped variant needs a threadId→project resolve inside the handler.
Auto-arming the prompt on every new draft.
Detecting an existing ~/.claude/commands/kickoff.md and offering to import it.
Alternatives considered
Native Claude Code slash commands already solve most of the insertion half. Honestly.
This is the strongest alternative and it deserves to be stated plainly rather than buried.
~/.claude/commands/kickoff.md containing the preamble plus $ARGUMENTS, then typing /kickoff my app idea in T3's composer with autocomplete, works today with zero code. The chain is verified end to end: ClaudeAdapter.ts:3528-3529 sets systemPrompt: { type: "preset", preset: "claude_code" } and settingSources: [...CLAUDE_SETTING_SOURCES] (= ["user","project","local"], :884), so the CLI reads the user's whole filesystem config; ClaudeProvider.ts:747 harvests init.commands; ChatComposer.tsx:1099-1110 renders them in the / menu; ChatComposer.tsx:1728 inserts /<name> . I already have ~/.claude/commands/animate.md and it works.
If you only use Claude, that is 80% of this feature and you should just do that. This issue's justification is the remaining 20%, which the slash-command path structurally cannot reach:
gap
why slash commands cannot close it
cross-provider
only ClaudeProvider produces slashCommands; Codex/Cursor/Grok/OpenCode get nothing
remote clients
~/.claude/commands is a host-filesystem directory; a phone paired to a remote server cannot see or edit it
transcript honesty
T3 sends /kickoff … verbatim; the CLI expands it out of band and T3 never sees the expansion
model-editable
a .md file the model could rewrite has no permission gate, no diff, no history, no revert
discoverability
you have to remember to type it
--append-system-prompt via Claude launch arguments — UNVERIFIED
Settings → Providers → Claude → Launch arguments is parsed with parseCliArgs(claudeSettings.launchArgs).flags (ClaudeAdapter.ts:3489) and handed to the SDK as extraArgs, documented as "Additional CLI arguments to pass to Claude Code" (@anthropic-ai/claude-agent-sdk@0.3.170, sdk.d.ts:1433). So --append-system-prompt "…" is plausibly a zero-code always-on preamble today.
Unverified: whether it takes effect at all, given the adapter also sets systemPrompt: { type: "preset", preset: "claude_code" } explicitly at :3528. The SDK exposes a first-class append field on that same preset object (sdk.d.ts:1933: "{ type: 'preset', preset: 'claude_code', append: '...' } – Use default prompt with appended instructions"), which suggests the structured field, not the CLI flag, is the intended path — and that the flag may be overridden or conflict.
Experiment that settles it: put --append-system-prompt "Always begin every reply with the token XYZZY." in Claude launch args, start a thread, and see whether replies start with XYZZY. Five minutes.
Even if it works, it is the wrong product: always-on, invisible, on every message, Claude-only, and unreachable from a remote client. It is exactly option (b) that this issue rejects.
Prompt stash (Cmd+S)
apps/web/src/promptStashStore.ts is provider-agnostic by explicit design (:29-33) and already wired into the composer. But it is not a library: restore is a pop — "Removes and returns an entry from the queue (restore + delete)" (:205) — entries are unnamed, capped at 20 (:17), and stored in browser localStorage (:7). Per-client, not synced to mobile, not server-authoritative, invisible to models. Pinning a stash entry would need MAX_STASH_ENTRIES and pop semantics changed, and would still fail the Settings and model-edit requirements.
Put it in packages/contracts/src/settings.ts like sourceControlWritingStyle
The obvious move, and the wrong one for this fork. That file is ledger row +7/-2, churn 18, risk 162, it is a persisted schema, and docs/t3x/SEAMS.md names it as the anchor of the recurring issue-#29 rebase conflict. Adding a field costs two edits (the ServerSettings struct at :508 and ServerSettingsPatch at :649-653). The fork-owned JSON store + /api/t3x/* route pattern gets the same reach at zero contracts cost. Copy SourceControlWritingSettings.tsx as the UI template; do not copy its storage.
Have the model edit a file with its normal Write tool
autoResume does exactly this: .t3x/resume-prompt.md in the workspace, resolved server-side (autoResume/config.ts:82-110), editable by the agent under the provider's own permission prompt. Tempting because it costs nothing. Rejected here because the kickoff prompt is global, so it must live in the server state dir, not a workspace — and a raw Write to the state dir gives no schema validation, no proposal/accept step, no history, and no audit. The MCP toolkit is the price of a real permission story.
A /newproject built-in slash command
Rejected. There is no slash-command registry — packages/shared/src/composerTrigger.ts is a pure string module with no registry (:56-119, :137-149), and the built-in list is an inline array literal in ChatComposer.tsx:1076-1114 with a matching selection switch at :1706-1725. Two edits to a churn-34 file, roughly 10× the rebase cost of a hero-mounted chip for the same action.
#5049 ships a banner-slot + click-to-insert PoC (+463/−15). If it lands, the insert plumbing is free. But it is a turn-finished suggestion, not a new-project affordance, and it is still open with no maintainer signal. Not worth blocking on.
Risks or tradeoffs
Duplicate risk with two live upstream issues
The single biggest risk is this getting closed as a duplicate. See Duplicate search for the deltas — the issue must cite pingdotgg#1547 and pingdotgg#4490 by number in its opening.
Worth considering: pitch this as a superset of pingdotgg#1547 rather than beside it. A single-prompt design is strictly less code than a grouped snippet library, pingdotgg#1547's author volunteered to implement, and pingdotgg#1547 itself named its main risk as "added composer complexity and avoiding overlap/confusion with skills, slash commands, or automations" — which two competing composer surfaces would make worse.
Prompt injection → proposal spam
A hostile repo could steer the model into proposing a poisoned kickoff prompt ("always run this script first"). Mitigations, in order of strength:
No tool mutates the live prompt. Propose-only is the whole design. The blast radius is a proposal I must read.
Default OFF.modelEditingEnabled: false until explicitly turned on in Settings.
Diff + rationale + originating thread shown before accept.
History + one-click revert after accept.
Residual: proposal spam as a nuisance vector. Cap at one pending proposal, overwrite-latest, and record which thread proposed it.
Two new ledger rows against an explicit tripwire
docs/t3x/SEAMS.md is at 34 rows (+1616/-187) and says: "Before adding row 35, re-isolate something instead. Prefer fork-owned files even when an in-place edit is smaller."
Facing that squarely: DraftHeroHeadline.tsx and BetaSettingsPanel.tsx would be rows 35 and 36, at combined risk ≈ 34 — the two cheapest mounts available (churn 5 and 3; the alternatives are 63 and 18). Everything else is fork-owned or grows an existing row.
The re-isolation the tripwire asks for is already named in the ledger itself:docs/user/providers-claude.md (+86/-33, churn 1, risk 119) is annotated "the one row worth upstreaming, which would remove it." Land that upstream PR alongside this feature and the ledger nets 34 → 35 rows with lower total risk. If a reviewer will not accept two rows, drop the hero mount for _chat.draft.$draftId.tsx (churn 2, risk ≈ 4) and accept absolute positioning.
apps/server/src/mcp is under active upstream churn, including a rename
20+ commits in 90 days, including f7c422d58 "Refactor MCP services into top-level modules". That is exactly the SEAMS.md "upstream can RENAME a seam file" hazard the fork has already been bitten by. Mitigation: depend only on McpHttpServer.layer's public output; never import from McpInvocationContext.ts, McpSessionRegistry.ts, or toolkits/preview/. (Requiring McpInvocationContext in a tool's dependencies is unavoidable if the tool wants caller identity — accept that one import, or skip caller identity entirely for v1 since a global prompt does not need it.)
The MCP mount at server.ts:427 is UNVERIFIED
The reasoning is sound — McpServer.layerHttp outputs McpServer | McpServerClient, and McpHttpServer.layer composes it with Layer.provideMerge, so McpServer is in the output — but it has not been compiled.
Experiment: add a one-tool stub toolkit under apps/server/src/t3x/kickoffPrompt/mcp/, wire it as T3xMcpToolkitsLive.pipe(Layer.provideMerge(McpHttpServer.layer.pipe(Layer.provide(McpSessionRegistry.layer)))) at server.ts:427, and run the server typecheck. If an unsatisfied McpServer requirement leaks into makeRoutesLayer's type, fall back to a 2-line edit in apps/server/src/mcp/McpHttpServer.ts (import + one Layer.mergeAll member) — a third new row at churn 6, still cheap. Do this before committing to Phase 2's design.
#3164 "Add Automations & Triggers (for loops)" is labelled 🚧 In Progress upstream and its body says "It could also support template triggers and automation/triggers that can be reused." If automations ship a reusable prompt-template object, a separate global-prompt store becomes a parallel path — the fork's recorded recurring hazard, where a fork path that duplicates an upstream capability silently bypasses new upstream guards. Check pingdotgg#3164's branch state before Phase 1 lands (upstream PR pingdotgg#3638 shipped schedule_task/delegate_task on a stack branch, not main — the same trap already caught issues #42/#43).
Note the prefill design reduces this hazard on the send side: the chip writes to upstream's own composer store and the message goes through upstream's own onSend, so there is no parallel send path to keep in sync — unlike the fork's outbox.
Smaller edges
setPrompt replaces, it does not append. Tapping the chip on a non-empty draft would destroy typed text. Handle it (prepend + blank line) or the feature is a data-loss bug.
Composer drafts persist to localStorage (t3code:composer-drafts:v1). Prefilled text survives reload — good — but a stale copy also survives a later Settings edit. Acceptable; do not try to reconcile.
OpenCode coverage gap.OpenCodeAdapter.ts:1218 registers t3-code only if (mcpSession && !server.external), so an externally-hosted OpenCode server gets no fork tools. Phase 1 (prefill) is unaffected; Phase 2's model-edit half silently does not exist there. Say so in the Settings copy.
MCP tool-name classification.classifyRequestType (ClaudeAdapter.ts:652) is substring-based on the raw tool name (isReadOnlyToolName matches "read"/"view"/"grep"/"glob"/"search"). Naming a tool *_read would misfile it as file_read_approval in non-full-access modes. t3x_kickoff_prompt_get / _propose avoid this.
No audit trail. Nothing in the MCP path records tool-call side effects to a user-visible thread history. A model proposing an edit to my global prompt is arguably worth an inline timeline entry — undecided, and out of scope for v1.
useComposerDraftStore is web-only. Any future mobile parity needs a separate prefill path; do not design Phase 1 as if it will be shared.
Examples or references
Upstream issues to cite (all verified live on 2026-08-02)
[Feature]: Saved snippets for frequently used prompts, with optional grouping pingdotgg/t3code#1547 — OPEN, enhancement/needs-triage. "Saved snippets for frequently used prompts, with optional grouping". Closest neighbour, ~70% overlap. "snippets are searchable from the composer / selecting one inserts it into the current input / it does not auto-send". Author checked "I would be open to helping implement this."
apps/web/src/components/ChatView.tsx:2421isDraftHeroState, :6046-6062 hero render block, :4886/:4929/:5057outgoingMessageText (the send path this design deliberately avoids)
apps/web/src/components/chat/DraftHeroHeadline.tsx:27-35 props, :147-157 the bare <h1> return
apps/web/src/routes/_chat.draft.$draftId.tsx:85-88 — cheaper alternative mount
Settings
apps/web/src/components/settings/BetaSettingsPanel.tsx — <SettingsPageContainer><SettingsSection> shape to nest inside
apps/web/src/components/settings/settingsLayout.tsx:91SettingsSection, :125SettingsRow, :175SettingResetButton, :199-222SettingsPageContainer (flex-1 overflow-y-auto — the two-scroll-pane hazard)
apps/web/src/components/settings/settingsSearch.ts — the catalog v1 skips
Fork patterns to copy
apps/server/src/t3x/index.ts — the aggregator; T3xLayerLive / T3xRoutesLive, and the memoisation note explaining why store layers are module-scoped values
apps/server/src/t3x/autoResume/state.ts:1-14 (why a JSON file, not a DB migration), :44-53 (withDecodingDefaultKey or the whole file fails to decode), :100makeAutoResumeStore
apps/server/src/t3x/autoResume/http.ts:5-10 (raw route vs WS-RPC rationale), :38-57 (authenticateWithOperateScope mirror)
apps/server/src/mcp/McpInvocationContext.ts:10 closed McpCapability union (do not widen), :12-19 scope fields, :26-40requireMcpCapability as the error template
Five adapter mounts: ClaudeAdapter.ts:3552, CursorAdapter.ts:547, GrokAdapter.ts:585, OpenCodeAdapter.ts:1221 (gated by !server.external at :1218), CodexAdapter.test.ts:579
apps/server/src/provider/Layers/ClaudeAdapter.ts:3372-3378 — the full-access short-circuit that forces an in-handler gate
apps/server/src/provider/CodexDeveloperInstructions.ts:7 — the Codex-only tool advertisement
Seam ledger — docs/t3x/SEAMS.md: header totals (34 files, +1616/-187, merge-base 64bf01619), the row-35 tripwire, "Reading the risk column", and the docs/user/providers-claude.md row annotated as the one worth upstreaming.
Churn figures in this issue were re-measured on 2026-08-02 with the ledger's own recipe: MB=$(git merge-base main upstream/main); MBTS=$(git show -s --format=%ct $MB); git log --oneline --since="@$((MBTS-60*86400))" $MB -- <path> | wc -l.
Duplicate search performed before filing
Fork (radroid/t3code): no duplicate, no overlap.gh issue list --repo radroid/t3code --state all --limit 100 returns exactly 24 issues (#4-#45). None concerns prompts, prompt storage, composer prefill, or settings-owned text. Closest by theme: #43 (a fork-owned MCP toolkit for orchestration) and #36 (voice input into the composer, closed NOT_PLANNED) — both touch machinery this feature reuses; neither is a duplicate. #42 and #43 are in fact unblocked by this feature's T3xMcpToolkitsLive mount.
Upstream (pingdotgg/t3code): no exact duplicate, but two open issues own roughly half each. The full 1623-issue title corpus was dumped (gh issue list --state all --limit 6000 --json number,title,state,url) and grepped against 32 terms, yielding 65 candidates; ~35 additional body-level gh search issues queries were run.
Deltas that must appear in the issue body:
[Feature]: Saved snippets for frequently used prompts, with optional grouping pingdotgg/t3code#1547 (OPEN) — the composer-insert half, ~70% overlap and the most likely close-as-duplicate. Delta: N named snippets, manually inserted, no settings-owned single prompt, no model-edit half. This is one named preamble, armed at the new-project moment, with a model-writable proposal path. Consider pitching as a superset rather than a competitor.
Support local Codex custom prompts and $skills in the composer pingdotgg/t3code#737 (CLOSED/COMPLETED) — reusable-prompt-into-composer for Codex only, sourced from ~/.codex/prompts/*.md. Must be cited and differentiated (provider-agnostic, T3-owned storage, Settings-editable, model-editable). Caveat: not visible in the tree at merge-base 64bf01619.
[Feature]: Support for a global agent prompt (e.g. ~/.claude/CLAUDE.md) pingdotgg/t3code#1233 (CLOSED/COMPLETED) — the closest closed precedent for one global user-owned prompt, and it explicitly floated "a global prompt option in the user settings that works regardless of the agent provider". Closed with no linked PR or commit. The issue must state what remains uncovered: visible in the composer, editable from a remote client, editable by the model.
The model-editable half has zero prior art in either repo. Empty result sets across ~35 body-level queries covering: prompt library, saved prompt, custom instructions, project instructions, prefill, composer default, output style, default prompt, meta prompt, prompt enhance, improve my prompt, starter prompt, project kickoff, onboarding prompt, app idea, self-improving, user memory, macro. No open PR upstream matches "prompt template" or "saved prompt snippet" either. That property — a model revising the user's own saved prompt under an explicit, default-OFF permission with diff, history and revert — is the genuinely novel part and should carry the issue's justification.
Best-specified issue in the backlog and self-contained (apps/web + one contract setting), with no upstream dependency. This is the feature to build first once the keep-alive (#49) and active bugs (#39, #41) are cleared.
Triage 2026-08-07 — the composer geometry this is specified against has moved upstream
The design's central UX call — a chip under the draft hero that prefills the composer with visible text — rests on specific structure in ChatView.tsx and ChatComposer.tsx (isDraftHeroState at ChatView.tsx:2421, <DraftHeroHeadline> rendered above the composer at :6058, useComposerThreadDraft at ChatComposer.tsx:707). All of those line numbers are against the 2026-08-02 base.
upstream/main is 103 commits ahead and unmerged (#49). Inside that range:
48aa875c feat(web): remove Build/Plan toggle from the composer (#5551) — removes a control from exactly the region this feature wants to add one to.
a8cd2ad2 fix(web): plans stop hijacking the UI, fold into chat instead (#5558) — changes what the chat surface does with plan mode.
9235c83e fix(web): keep the composer command menu anchored to the composer (#5336) — touches the / menu this issue's "what already works" section §1 depends on.
85b1734d feat(web): add modular theme library (#5226) and ab3b55e2 also touch the composer.
ChatView.tsx takes 20 upstream commits in this range in total and is the file the last three sync attempts stopped on.
None of that invalidates the design. The argument for (a) visible prefill over (b) invisible prepend is structural — the client assembles the final string in onSend and the same string builds the optimistic bubble — and that reasoning survives a refactor. But the "prefill is one line and requires zero composer edits" claim must be re-verified after #49, because it is the entire cost case for this feature, and 48aa875c is a composer edit in the same neighbourhood.
Recommend: leave open, re-verify the zero-edit claim post-sync, then build. The Settings editor half (§2's sourceControlWritingStyle.customInstructions precedent) is unaffected and could be specified independently — though note packages/contracts/src/settings.ts takes 4 upstream commits in this range and was a conflict file in the 08-07 sync run, so it is not a free surface right now either.
The rename landed: fork code is apps/server/src/coil/, apps/web/src/components/coil/, docs at docs/coil/SEAMS.md (34-row ledger is gone — check current header before citing a row number).
BetaSettingsPanel.tsx no longer exists — the §2 mount point is dead. The loops feature (feat(coil): loops — a durable supervisor for unattended threads #133) established the replacement pattern instead: a dedicated route apps/web/src/routes/settings.loops.tsx rendering apps/web/src/components/coil/LoopsSettings.tsx, registered in settingsSearch.ts and SettingsSidebarNav.tsx. Those two files are now already fork rows (loops paid that cost), so a /settings/kickoff-prompt entry (or folding into a shared "Coil" settings page) is an incremental add to an already-touched file, not a new ledger row — the calculus in the issue's "two new ledger rows" risk section is stale and more favorable now.
The composer-prefill anchors mostly hold: isDraftHeroState still exists in ChatView.tsx (now computed via resolveDraftHeroState around line ~2867, consumed ~6202), DraftHeroHeadline.tsx is still a fork-editable component receiving activeProjectRef/draftId, and useComposerThreadDraft/useComposerDraftStore/setPrompt all still exist in composerDraftStore.ts (just renumbered). The "prefill is one line, zero composer edits" claim still checks out.
The MCP mount question is answered, not just verified: loops (feat(coil): loops — a durable supervisor for unattended threads #133) shipped a real toolkit at apps/server/src/mcp/toolkits/loop/ merged into apps/server/src/mcp/McpHttpServer.ts via Layer.mergeAll(PreviewToolkitRegistrationLive, LoopToolkitRegistrationLive /* coil fork seam */). That line is the aggregator this issue's Phase 2 wanted to build — copy that merge site directly instead of inventing T3xMcpToolkitsLive. Upstream meanwhile added its own device and pullRequests toolkits alongside preview, so apps/server/src/mcp/toolkits/ churn is higher than assumed — irrelevant to the fork row (still 0 for apps/server/src/coil/).
Upstream has not shipped anything overlapping this feature (no prompt-library, no kickoff mechanism). packages/contracts/src/settings.ts keeps growing (30 commits since the last sync's merge-base) — the "don't touch it" recommendation is stronger, not weaker.
Sync [coil-sync] daily sync needs attention (conflict) #135 (~867 commits behind as of today, not ~100 — the estimate in this task's framing is stale) is open with conflicts, unmerged. ChatView.tsx continues to be heavily touched upstream.
Recommended scope for v1
Ship exactly the issue's original Phase 1: server-side JSON store + /api/t3x/kickoff-prompt route + composer chip + a settings entry. Defer Phase 2 (model-editable proposals via MCP) to a follow-up now that the toolkit-merge pattern is proven — it's cheap to add later and doesn't need to block v1.
Phases
Server store + route (fork-owned only). Add apps/server/src/coil/kickoffPrompt/{state,http,config}.ts, copying coil/autoResume/state.ts and coil/webPush/http.ts verbatim for shape. Register in apps/server/src/coil/index.ts (CoilLayerLive/CoilRoutesLive). Zero upstream files. Tests: state round-trip + http auth-scope test mirroring autoResume/http.test.ts.
Web chip — apps/web/src/components/coil/KickoffPromptChip.tsx, mounted in DraftHeroHeadline.tsx (~5 lines). Verify current return shape before wrapping it (grep confirms it's still a plain function component, but re-check the JSX return, not just the props, since it moved). Tests: a store-level test that clicking prepends rather than replaces non-empty drafts.
Web settings entry — reuse the loops precedent: either add a route under settings.kickoff-prompt.tsx or fold a <KickoffPromptSettings> section into an existing fork settings page if one is shared. Register in settingsSearch.ts + SettingsSidebarNav.tsx (already-touched files per above). Surfaces: web only for v1; mobile explicitly deferred (composer draft store is web-only); desktop inherits web automatically.
(Follow-up issue, not this v1) MCP propose tool — apps/server/src/mcp/toolkits/kickoffPrompt/{tools,handlers}.ts, merged at the now-proven McpHttpServer.ts toolkit-merge site. Default-off modelEditingEnabled, propose-only, no mutating tool. Provider surfaces: all five get it for free via the shared MCP mount (unchanged from the original design).
Decisions the maintainer must make
Where does kickoff-prompt settings live? A new dedicated route (matches loops' precedent, cheap) vs. folding into a general "Coil" settings page if one gets built for multiple small toggles. Recommend: dedicated route now, consolidate later if the fork accumulates a third small settings surface.
Ship Phase 2 (model-editable) now or as a separate issue?Recommend: separate issue — the toolkit-merge pattern is proven, so there's no risk in deferring, and v1 alone closes 4 of the 5 gaps the issue names.
Blockers / sequencing
No hard blocker. Independent of sync #135 (touches zero upstream files that are mid-conflict). Should land before or alongside any future MCP toolkit work since it would be only the second fork toolkit to use the merge site loops established — good to prove the pattern generalizes.
Before submitting
Area
apps/web
Problem or use case
Starting a new project is the one moment T3 Code has the least reuse to offer, and the workarounds that exist are each scoped to something that makes them not-quite-work.
I keep re-typing (or re-pasting) the same "help me plan this app idea" preamble at the start of every new thread. What I want is one prompt I own, that shows up at the new-project moment, that I can read and edit in Settings from any client, and that gets better over time because the model that just used it can suggest an improvement.
T3 already ships three overlapping pieces of this, and none of them is the whole thing. Being precise about what already works matters, because the honest version of this request is narrow.
What already works (do not rebuild these):
ClaudeAdaptersets theclaude_codepreset and all three setting sources (apps/server/src/provider/Layers/ClaudeAdapter.ts:3528-3529, withCLAUDE_SETTING_SOURCESat:884), so the CLI inherits~/.claude/commands/*.md,CLAUDE.md, skills, agents and hooks.ClaudeProviderthen harvestsinit.commandsinto the composer's/menu (apps/server/src/provider/Layers/ClaudeProvider.ts:747), consumed atapps/web/src/components/chat/ChatComposer.tsx:1099-1110. Selecting one inserts the literal/<name>(ChatComposer.tsx:1728) and the CLI expands it. So~/.claude/commands/kickoff.mdgenuinely is a working, autocompleted kickoff prompt — I already have one such file (~/.claude/commands/animate.md), so I know the shape works.sourceControlWritingStyle.customInstructionsis a Settings textarea whose text is fed to a model (packages/contracts/src/settings.ts:421-429, UIapps/web/src/components/settings/SourceControlWritingSettings.tsx:118-136, consumedapps/server/src/git/GitManager.ts:612-615).t3code:prompt-stash:v2(apps/web/src/promptStashStore.ts:7,:17).The four gaps those leave, each with evidence:
command grep -rn "slashCommands" apps/server/src/provider/Layers/returns producers only inClaudeProvider.ts(:747,:904-935).CodexProvidersurfaces skills (client.request("skills/list")atCodexProvider.ts:394) but neverslashCommands;CursorProvider/GrokProvider/OpenCodeProviderhave neither. A Codex, Cursor, Grok or OpenCode user has no equivalent at all.~/.claude/commandsis a directory on the host filesystem. A phone or a browser paired to a remote T3 server can neither see nor edit it./kickoff …verbatim and the CLI expands it out of band, so neither T3 nor I ever see what was actually sent.Plus a fifth, softer one: discoverability. A kickoff prompt only helps if it is present when I start a new project. A slash command I have to remember to type is not.
Proposed solution
The shape: a "kickoff prompt"
One named, fork-owned, server-stored preamble. Three surfaces: a chip at the new-project moment, an editor in Settings, and (phase 2) two MCP tools so the model can propose a revision.
The central UX call — committed: visible prefill, triggered by a chip. Not an invisible prepend.
Three candidates were on the table:
Ship (c) as the trigger and (a) as the behaviour: a chip under the draft hero that prefills the composer with visible, editable text. Reject (b) outright.
Evidence for that call:
onSendand sends it asthread.turn.start(packages/client-runtime/src/operations/commands.ts:232-242); the same string builds the optimistic user bubble (apps/web/src/components/ChatView.tsx:4886,:4929,:5057). A server-side prepend makes the rendered transcript disagree with what the model received. A client-side prepend means editingChatView.tsx— the fork's worst seam at risk 11466.apps/web/src/components/chat/ChatComposer.tsx:707useComposerThreadDraft(composerDraftTarget),:708,:716store.setPrompt), and the Lexical editor is fully controlled — a layout effect rewrites editor state whenever the externalvaluediffers (apps/web/src/components/ComposerPromptEditor.tsx:1605-1610,$setComposerEditorPrompt(value, …)). Upstream already calls the store imperatively from outside React (apps/web/src/composerDraftStore.ts:3541). Prefill is one line and requires zero composer edits.--append-system-promptin Claude launch args would give you (see Alternatives) — riding along on every message, invisible, undebuggable.isDraftHeroState(ChatView.tsx:2421) renders<DraftHeroHeadline>— "What should we build in {project}?" — directly above the composer (ChatView.tsx:6058). That is literally the new-project moment.Explicitly not v1: auto-arming on every new draft. Composer drafts persist to
localStorageundert3code:composer-drafts:v1(composerDraftStore.ts:59), so auto-prefilled text is indistinguishable from text I typed, and I would have to delete a wall of text every time I just want to ask a quick question. Revisit as an opt-in toggle later.Storage — fork-owned, never
packages/contracts/src/settings.tsCopy
apps/server/src/t3x/autoResume/state.tsline for line: a single JSON file in the server state dir, an EffectSchema.Struct, mutations serialised through aSynchronizedRefand persisted atomically viaapps/server/src/atomicWrite.ts.apps/server/src/t3x/kickoffPrompt/state.ts→<config.stateDir>/t3x-kickoff-prompt.json:Every new field must decode with
Schema.withDecodingDefaultKey—autoResume/state.ts:44-53documents why: a missing required key fails the whole-file decode and the boot path silently drops everything.Wire it in
apps/server/src/t3x/index.tsexactly likeAutoResumeStoreLive(Layer.effectreadingServerConfig.stateDir+Path.Path). Zero new upstream rows.HTTP —
/api/t3x/kickoff-promptapps/server/src/t3x/kickoffPrompt/http.ts, modelled onapps/server/src/t3x/autoResume/http.ts:GET→{ text, updatedAtMs, modelEditingEnabled, pendingProposal, history }POST→{ text?, modelEditingEnabled?, acceptProposal?, rejectProposal?, revertToVersionAtMs? }Reuse the
authenticateWithOperateScopemirror (autoResume/http.ts:38-57) — it is already a documented logic mirror indocs/t3x/SEAMS.md. Register throughT3xRoutesLive. Zero new upstream rows. A raw route rather than a WS-RPC method, for the reasonautoResume/http.ts:5-10gives: an RPC would force edits to@t3tools/contractsplusapps/server/src/ws.ts(churn 41).Web — chip
apps/web/src/t3x/KickoffPromptChip.tsx(fork-owned). Mounted inapps/web/src/components/chat/DraftHeroHeadline.tsx, which today returns a bare<h1>(:147-157) — wrap it in a fragment and append the chip. That component already receivesactiveProjectRef(:27-35), so noChatView.tsxedit is needed.Resolving the composer target: prefer the route param via TanStack Router (
useParams({ from: "/_chat/draft/$draftId", shouldThrow: false })) read inside the fork component; fall back touseComposerDraftStore.getState().getDraftThreadByProjectRef(activeProjectRef)?.draftId(composerDraftStore.ts:338,:2213;ProjectDraftSession extends DraftSessionState { draftId: DraftId }at:306-308).ComposerThreadTarget = ScopedThreadRef | DraftId(:318) is not exported, but a bareDraftIdis assignable, so no upstream export is needed.On click:
Two behaviours the implementer must get right:
setPromptreplaces. IfgetComposerDraft(target)?.promptis non-empty, prependkickoffText + "\n\n"instead of replacing.The chip needs
pointer-events-autoto match its siblings — the hero sits inside anabsolute inset-x-0 bottom-fullblock (ChatView.tsx:6047) and the interactive elements insideDraftHeroHeadlineall carry that class (:98,:141).Cheaper alternative mount if the hero edit is rejected:
apps/web/src/routes/_chat.draft.$draftId.tsx(churn 2) already holdsdraftIdand renders<ChatView>, following the provenAutoResumeOverlaysibling pattern (apps/web/src/routes/_chat.$environmentId.$threadId.tsx:18,:92). Tradeoff: absolute positioning instead of natural in-flow placement under the headline.Web — Settings editor
apps/web/src/t3x/KickoffPromptSettings.tsx, exported as a bare<SettingsSection>, never its own<SettingsPageContainer>— that container isflex-1 overflow-y-auto(apps/web/src/components/settings/settingsLayout.tsx:199-222), so two siblings on one route produce two competing scroll panes.Mount inside
BetaSettingsPanel's existing container (apps/web/src/components/settings/BetaSettingsPanel.tsx, which already renders<SettingsPageContainer><SettingsSection>). Churn 3 — the cheapest settings surface in the tree. A brand-new settings page would cost ~10 churn instead (exhaustiveRecord<SettingsPath, …>entries in bothsettingsSearch.tsandSettingsSidebarNav.tsx, churn 9).Contents:
SourceControlWritingSettings.tsx:118-136verbatim: uncontrolled<Textarea key={text} defaultValue={text} onBlur={commit} rows={…} aria-label>nested inside a<SettingsRow>.apps/web/src/components/ui/textarea.tsxis churn 0.<SettingResetButton>(settingsLayout.tsx:175) → reset to the shipped default.Phase 2 — the model-editable half, as a fork-owned MCP toolkit
This is the half with no prior art anywhere, and it is provider-agnostic for free: all five adapters already mount a per-thread
t3-codeHTTP MCP server —ClaudeAdapter.ts:3552,CursorAdapter.ts:547,GrokAdapter.ts:585,OpenCodeAdapter.ts:1221, and Codex viamcp_servers.t3-code.url=…(asserted inCodexAdapter.test.ts:579). The toolkit directory today contains onlypreview/.apps/server/src/t3x/kickoffPrompt/mcp/{tools,handlers}.ts, following theTool.make(...)/Toolkit.make(...)/Toolkit.toLayer(handlers)shape inapps/server/src/mcp/toolkits/preview/:t3x_kickoff_prompt_get— returns the current text.Tool.Readonly true.t3x_kickoff_prompt_propose—{ text, rationale }. WritespendingProposal. Returns a message saying it is queued for the user's review and is not live.There is deliberately no tool that mutates the live prompt. The worst a prompt-injected or compromised model can do is queue a proposal I have to read and accept. That is the whole permission story, and it is stronger than anything an approval dialog could give.
The opt-in gate must be enforced inside the fork's handler, not delegated to T3's approval system. Two verified reasons:
apps/server/src/provider/Layers/ClaudeAdapter.ts:3372-3378—runtimeMode === "full-access"returns{ behavior: "allow" }before anyApprovalRequestIdis minted. Most threads run in full-access.So: read
modelEditingEnabledfrom the store inside the handler and fail with a typed error the model can read and explain — the same shape asrequireMcpCapability(apps/server/src/mcp/McpInvocationContext.ts:26-40), but keyed on a persisted setting rather than the issued capability set. Do not widenMcpCapability(McpInvocationContext.ts:10, churn 3) or the hardcoded issued set (McpSessionRegistry.ts:131, churn 7) — that would cost two extra ledger rows.Caller identity comes free and cannot be spoofed:
McpInvocationContextcarriesenvironmentId/threadId/providerSessionId/providerInstanceId(McpInvocationContext.ts:12-19), bound to the request by bearer token in the auth middleware (McpHttpServer.ts:70-89). It does not carryprojectId.Mount point (UNVERIFIED — see Risks).
McpServer.layerHttpoutputs theMcpServerservice, andMcpHttpServer.layer = PreviewToolkitRegistrationLive.pipe(Layer.provideMerge(McpTransportLive))(apps/server/src/mcp/McpHttpServer.ts:206-225), soMcpServeris in that layer's public output. Thereforeapps/server/src/server.ts:427:server.tsis already a ledger row (+3 lines, churn 29, risk 87) — this grows it rather than adding a row, and keepsapps/server/src/mcp/*untouched.Tool discoverability across providers. The only place T3 tells a model the
t3-codeserver exists isapps/server/src/provider/CodexDeveloperInstructions.ts:7, and it is Codex-only. Rather than add per-adapter hints, let the kickoff prompt advertise its own tools: whenmodelEditingEnabledis on, append a line to the resolved text. That is fork-owned data, zero seam, and works on all five providers:Default prompt
Ship a working default so the feature needs zero setup (
autoResume'sDEFAULT_RESUME_PROMPT = "continue"atconfig.ts:106is the precedent). Something like:Why this matters
Who benefits, concretely:
ClaudeProviderpopulatesslashCommands(ClaudeProvider.ts:747). This works identically on all five providers because it is plain text written into upstream's own composer store.~/.claude/commands/*.mdlives on the host filesystem and is unreachable from a browser or phone paired to a remote T3 server./api/t3x/kickoff-promptis reachable from any paired client, over the sameprimaryEnvironmentHttpLayerpathAutoResumeOverlayalready uses./kickoffpath can never offer that, because the CLI expands the command out of band and T3 never sees the expansion.What it unlocks for the fork specifically: this is the first fork feature to add a
t3-codeMCP tool. OnceT3xMcpToolkitsLiveexists and compiles, issues #42 (wake_me) and #43 (provider-agnostic orchestration) inherit the mount for free — they are both blocked on exactly this plumbing.Smallest useful scope
Phase 1 — no MCP, no model editing, no history. Ship this alone; it fixes four of the five gaps.
apps/server/src/t3x/kickoffPrompt/state.ts— JSON store at<stateDir>/t3x-kickoff-prompt.json,{ version: 1, text, updatedAtMs }only. CopyautoResume/state.ts.apps/server/src/t3x/kickoffPrompt/http.ts—GET/POST/api/t3x/kickoff-prompt, operate scope, registered throughT3xRoutesLiveinapps/server/src/t3x/index.ts.apps/web/src/t3x/KickoffPromptChip.tsx+ a ~5-line mount inDraftHeroHeadline.tsx(wrap the<h1>at:147-157in a fragment).apps/web/src/t3x/KickoffPromptSettings.tsx(bare<SettingsSection>: textarea + reset) + a ~3-line mount insideBetaSettingsPanel.tsx's existing container.Upstream cost of Phase 1: two new ledger rows, ~8 changed lines, combined risk ≈ 34. Zero server rows, zero contracts rows, zero
ChatView.tsx, zeroChatComposer.tsx.apps/web/src/components/chat/DraftHeroHeadline.tsxapps/web/src/components/settings/BetaSettingsPanel.tsx(Churn re-measured against merge-base
64bf01619with thedocs/t3x/SEAMS.mdrecipe on 2026-08-02.)Phase 2 — the model-editable half.
modelEditingEnabledswitch (default OFF), the two MCP tools,pendingProposal+ accept/reject diff card,history+ one-click revert, and the tool-advertising suffix on the resolved prompt. Adds ~2 lines to the existingapps/server/src/server.tsrow (risk +~58).Deliberately deferred, in priority order:
apps/mobile/src/features/threads/ThreadComposer.tsxis churn 16 / risk 480 and already a fork seam. Web-only for v1.apps/web/src/components/settings/settingsSearch.ts, churn 1). Near-free in risk terms but it is a third new ledger row against an explicit tripwire; the fork's existing settings row already made this same call and is unfindable via search. Judgement call, revisit.promptStashStore.ts's 20-entry cap hints the latent need is a small set, but a single-prompt v1 is strictly less code and generalises cleanly into upstream [Feature]: Saved snippets for frequently used prompts, with optional grouping pingdotgg/t3code#1547's list later.t3.json(packages/contracts/src/t3ProjectFile.ts:61-86— it has$schema,iconPath,scripts, no prompt field). Also noteMcpInvocationScopecarriesthreadIdbut notprojectId, so a project-scoped variant needs a threadId→project resolve inside the handler.~/.claude/commands/kickoff.mdand offering to import it.Alternatives considered
Native Claude Code slash commands already solve most of the insertion half. Honestly.
This is the strongest alternative and it deserves to be stated plainly rather than buried.
~/.claude/commands/kickoff.mdcontaining the preamble plus$ARGUMENTS, then typing/kickoff my app ideain T3's composer with autocomplete, works today with zero code. The chain is verified end to end:ClaudeAdapter.ts:3528-3529setssystemPrompt: { type: "preset", preset: "claude_code" }andsettingSources: [...CLAUDE_SETTING_SOURCES](=["user","project","local"],:884), so the CLI reads the user's whole filesystem config;ClaudeProvider.ts:747harvestsinit.commands;ChatComposer.tsx:1099-1110renders them in the/menu;ChatComposer.tsx:1728inserts/<name>. I already have~/.claude/commands/animate.mdand it works.If you only use Claude, that is 80% of this feature and you should just do that. This issue's justification is the remaining 20%, which the slash-command path structurally cannot reach:
ClaudeProviderproducesslashCommands; Codex/Cursor/Grok/OpenCode get nothing~/.claude/commandsis a host-filesystem directory; a phone paired to a remote server cannot see or edit it/kickoff …verbatim; the CLI expands it out of band and T3 never sees the expansion.mdfile the model could rewrite has no permission gate, no diff, no history, no revert--append-system-promptvia Claude launch arguments — UNVERIFIEDSettings → Providers → Claude → Launch arguments is parsed with
parseCliArgs(claudeSettings.launchArgs).flags(ClaudeAdapter.ts:3489) and handed to the SDK asextraArgs, documented as "Additional CLI arguments to pass to Claude Code" (@anthropic-ai/claude-agent-sdk@0.3.170,sdk.d.ts:1433). So--append-system-prompt "…"is plausibly a zero-code always-on preamble today.Unverified: whether it takes effect at all, given the adapter also sets
systemPrompt: { type: "preset", preset: "claude_code" }explicitly at:3528. The SDK exposes a first-classappendfield on that same preset object (sdk.d.ts:1933: "{ type: 'preset', preset: 'claude_code', append: '...' }– Use default prompt with appended instructions"), which suggests the structured field, not the CLI flag, is the intended path — and that the flag may be overridden or conflict.Experiment that settles it: put
--append-system-prompt "Always begin every reply with the token XYZZY."in Claude launch args, start a thread, and see whether replies start withXYZZY. Five minutes.Even if it works, it is the wrong product: always-on, invisible, on every message, Claude-only, and unreachable from a remote client. It is exactly option (b) that this issue rejects.
Prompt stash (
Cmd+S)apps/web/src/promptStashStore.tsis provider-agnostic by explicit design (:29-33) and already wired into the composer. But it is not a library: restore is a pop — "Removes and returns an entry from the queue (restore + delete)" (:205) — entries are unnamed, capped at 20 (:17), and stored in browserlocalStorage(:7). Per-client, not synced to mobile, not server-authoritative, invisible to models. Pinning a stash entry would needMAX_STASH_ENTRIESand pop semantics changed, and would still fail the Settings and model-edit requirements.Put it in
packages/contracts/src/settings.tslikesourceControlWritingStyleThe obvious move, and the wrong one for this fork. That file is ledger row
+7/-2, churn 18, risk 162, it is a persisted schema, anddocs/t3x/SEAMS.mdnames it as the anchor of the recurring issue-#29 rebase conflict. Adding a field costs two edits (theServerSettingsstruct at:508andServerSettingsPatchat:649-653). The fork-owned JSON store +/api/t3x/*route pattern gets the same reach at zero contracts cost. CopySourceControlWritingSettings.tsxas the UI template; do not copy its storage.Have the model edit a file with its normal Write tool
autoResumedoes exactly this:.t3x/resume-prompt.mdin the workspace, resolved server-side (autoResume/config.ts:82-110), editable by the agent under the provider's own permission prompt. Tempting because it costs nothing. Rejected here because the kickoff prompt is global, so it must live in the server state dir, not a workspace — and a raw Write to the state dir gives no schema validation, no proposal/accept step, no history, and no audit. The MCP toolkit is the price of a real permission story.A
/newprojectbuilt-in slash commandRejected. There is no slash-command registry —
packages/shared/src/composerTrigger.tsis a pure string module with no registry (:56-119,:137-149), and the built-in list is an inline array literal inChatComposer.tsx:1076-1114with a matching selection switch at:1706-1725. Two edits to a churn-34 file, roughly 10× the rebase cost of a hero-mounted chip for the same action.Wait for upstream pingdotgg#5049's composer banner
#5049ships a banner-slot + click-to-insert PoC (+463/−15). If it lands, the insert plumbing is free. But it is a turn-finished suggestion, not a new-project affordance, and it is still open with no maintainer signal. Not worth blocking on.Risks or tradeoffs
Duplicate risk with two live upstream issues
The single biggest risk is this getting closed as a duplicate. See Duplicate search for the deltas — the issue must cite pingdotgg#1547 and pingdotgg#4490 by number in its opening.
Worth considering: pitch this as a superset of pingdotgg#1547 rather than beside it. A single-prompt design is strictly less code than a grouped snippet library, pingdotgg#1547's author volunteered to implement, and pingdotgg#1547 itself named its main risk as "added composer complexity and avoiding overlap/confusion with skills, slash commands, or automations" — which two competing composer surfaces would make worse.
Prompt injection → proposal spam
A hostile repo could steer the model into proposing a poisoned kickoff prompt ("always run this script first"). Mitigations, in order of strength:
modelEditingEnabled: falseuntil explicitly turned on in Settings.Residual: proposal spam as a nuisance vector. Cap at one pending proposal, overwrite-latest, and record which thread proposed it.
Two new ledger rows against an explicit tripwire
docs/t3x/SEAMS.mdis at 34 rows (+1616/-187) and says: "Before adding row 35, re-isolate something instead. Prefer fork-owned files even when an in-place edit is smaller."Facing that squarely:
DraftHeroHeadline.tsxandBetaSettingsPanel.tsxwould be rows 35 and 36, at combined risk ≈ 34 — the two cheapest mounts available (churn 5 and 3; the alternatives are 63 and 18). Everything else is fork-owned or grows an existing row.The re-isolation the tripwire asks for is already named in the ledger itself:
docs/user/providers-claude.md(+86/-33, churn 1, risk 119) is annotated "the one row worth upstreaming, which would remove it." Land that upstream PR alongside this feature and the ledger nets 34 → 35 rows with lower total risk. If a reviewer will not accept two rows, drop the hero mount for_chat.draft.$draftId.tsx(churn 2, risk ≈ 4) and accept absolute positioning.apps/server/src/mcpis under active upstream churn, including a rename20+ commits in 90 days, including
f7c422d58 "Refactor MCP services into top-level modules". That is exactly the SEAMS.md "upstream can RENAME a seam file" hazard the fork has already been bitten by. Mitigation: depend only onMcpHttpServer.layer's public output; never import fromMcpInvocationContext.ts,McpSessionRegistry.ts, ortoolkits/preview/. (RequiringMcpInvocationContextin a tool'sdependenciesis unavoidable if the tool wants caller identity — accept that one import, or skip caller identity entirely for v1 since a global prompt does not need it.)The MCP mount at
server.ts:427is UNVERIFIEDThe reasoning is sound —
McpServer.layerHttpoutputsMcpServer | McpServerClient, andMcpHttpServer.layercomposes it withLayer.provideMerge, soMcpServeris in the output — but it has not been compiled.Experiment: add a one-tool stub toolkit under
apps/server/src/t3x/kickoffPrompt/mcp/, wire it asT3xMcpToolkitsLive.pipe(Layer.provideMerge(McpHttpServer.layer.pipe(Layer.provide(McpSessionRegistry.layer))))atserver.ts:427, and run the server typecheck. If an unsatisfiedMcpServerrequirement leaks intomakeRoutesLayer's type, fall back to a 2-line edit inapps/server/src/mcp/McpHttpServer.ts(import + oneLayer.mergeAllmember) — a third new row at churn 6, still cheap. Do this before committing to Phase 2's design.Structural collision with upstream pingdotgg#3164
#3164 "Add Automations & Triggers (for loops)"is labelled🚧 In Progressupstream and its body says "It could also support template triggers and automation/triggers that can be reused." If automations ship a reusable prompt-template object, a separate global-prompt store becomes a parallel path — the fork's recorded recurring hazard, where a fork path that duplicates an upstream capability silently bypasses new upstream guards. Check pingdotgg#3164's branch state before Phase 1 lands (upstream PR pingdotgg#3638 shippedschedule_task/delegate_taskon a stack branch, not main — the same trap already caught issues #42/#43).Note the prefill design reduces this hazard on the send side: the chip writes to upstream's own composer store and the message goes through upstream's own
onSend, so there is no parallel send path to keep in sync — unlike the fork's outbox.Smaller edges
setPromptreplaces, it does not append. Tapping the chip on a non-empty draft would destroy typed text. Handle it (prepend + blank line) or the feature is a data-loss bug.localStorage(t3code:composer-drafts:v1). Prefilled text survives reload — good — but a stale copy also survives a later Settings edit. Acceptable; do not try to reconcile.OpenCodeAdapter.ts:1218registerst3-codeonlyif (mcpSession && !server.external), so an externally-hosted OpenCode server gets no fork tools. Phase 1 (prefill) is unaffected; Phase 2's model-edit half silently does not exist there. Say so in the Settings copy.classifyRequestType(ClaudeAdapter.ts:652) is substring-based on the raw tool name (isReadOnlyToolNamematches "read"/"view"/"grep"/"glob"/"search"). Naming a tool*_readwould misfile it asfile_read_approvalin non-full-access modes.t3x_kickoff_prompt_get/_proposeavoid this.useComposerDraftStoreis web-only. Any future mobile parity needs a separate prefill path; do not design Phase 1 as if it will be shared.Examples or references
Upstream issues to cite (all verified live on 2026-08-02)
enhancement/needs-triage. "Saved snippets for frequently used prompts, with optional grouping". Closest neighbour, ~70% overlap. "snippets are searchable from the composer / selecting one inserts it into the current input / it does not auto-send". Author checked "I would be open to helping implement this."enhancement/needs-triage. "Configurable instruction files injected into the system prompt, per provider and per project". ~40% overlap, opposite delivery (invisible system-prompt injection).main...kakismash:t3code:feat/suggested-next-prompt(+463/−15).settingSources: ["user","project","local"]making the Claude CLI read~/.claude/CLAUDE.mditself, which is Claude-only and invisible in the composer./prompts:<name>from~/.codex/prompts/*.md). Note: at merge-base64bf01619no provider layer other thanClaudeProviderpopulatesslashCommands, and nothing in the tree reads~/.codex/prompts— so whatever landed for Support local Codex custom prompts and $skills in the composer pingdotgg/t3code#737 is not visible in T3's composer today. Unverified; check before citing it as shipped prior art.🚧 In Progress. Automations & Triggers; mentions reusable "template triggers". Check its branch state before building.File:line evidence index for the implementer
Existing mechanisms (do not rebuild)
apps/server/src/provider/Layers/ClaudeAdapter.ts:884—CLAUDE_SETTING_SOURCES = ["user","project","local"];:3528-3529—claude_codepreset +settingSourcesapps/server/src/provider/Layers/ClaudeProvider.ts:747—slashCommands: parseClaudeInitializationCommands(init.commands)apps/web/src/components/chat/ChatComposer.tsx:1099-1110—providerSlashCommandItems;:1728—const replacement = `/${item.command.name} `apps/server/src/provider/Layers/CodexProvider.ts:394—client.request("skills/list", …)(Codex has skills, no slash commands)apps/web/src/promptStashStore.ts:7,:17,:205— stash key, 20-entry cap, pop-on-restorepackages/contracts/src/settings.ts:421-429,apps/web/src/components/settings/SourceControlWritingSettings.tsx:118-136,apps/server/src/git/GitManager.ts:612-615— the Settings-prompt precedentComposer prefill (the whole Phase 1 mechanism)
apps/web/src/components/chat/ChatComposer.tsx:707useComposerThreadDraft(composerDraftTarget),:708,:716store.setPromptapps/web/src/components/ComposerPromptEditor.tsx:1605-1610—shouldRewriteEditorState→$setComposerEditorPrompt(value, …)apps/web/src/composerDraftStore.ts:59storage key,:306-308ProjectDraftSession,:318ComposerThreadTarget,:338/:2213getDraftThreadByProjectRef,:404setPromptsignature,:3541imperativegetState()precedentapps/web/src/components/ChatView.tsx:2421isDraftHeroState,:6046-6062hero render block,:4886/:4929/:5057outgoingMessageText(the send path this design deliberately avoids)apps/web/src/components/chat/DraftHeroHeadline.tsx:27-35props,:147-157the bare<h1>returnapps/web/src/routes/_chat.draft.$draftId.tsx:85-88— cheaper alternative mountSettings
apps/web/src/components/settings/BetaSettingsPanel.tsx—<SettingsPageContainer><SettingsSection>shape to nest insideapps/web/src/components/settings/settingsLayout.tsx:91SettingsSection,:125SettingsRow,:175SettingResetButton,:199-222SettingsPageContainer(flex-1 overflow-y-auto— the two-scroll-pane hazard)apps/web/src/components/settings/settingsSearch.ts— the catalog v1 skipsFork patterns to copy
apps/server/src/t3x/index.ts— the aggregator;T3xLayerLive/T3xRoutesLive, and the memoisation note explaining why store layers are module-scoped valuesapps/server/src/t3x/autoResume/state.ts:1-14(why a JSON file, not a DB migration),:44-53(withDecodingDefaultKeyor the whole file fails to decode),:100makeAutoResumeStoreapps/server/src/t3x/autoResume/http.ts:5-10(raw route vs WS-RPC rationale),:38-57(authenticateWithOperateScopemirror)apps/server/src/t3x/autoResume/config.ts:106-110—DEFAULT_RESUME_PROMPT,resolveResumePromptprecedenceapps/web/src/t3x/AutoResumeOverlay.tsx+apps/web/src/routes/_chat.$environmentId.$threadId.tsx:18,:92— the fork-owned-component-mounted-on-a-route patternapps/server/src/server.ts:59,:224,:422(existing 3-line seam),:427(the MCP mount to grow)MCP
apps/server/src/mcp/toolkits/preview/tools.ts:27-52Tool.makeshape,:203-236Toolkit.makeapps/server/src/mcp/toolkits/preview/handlers.ts:63-98handler record +satisfies Parameters<typeof X.toLayer>[0]apps/server/src/mcp/McpHttpServer.ts:70-89bearer-token →McpInvocationContext,:206-225layer compositionapps/server/src/mcp/McpInvocationContext.ts:10closedMcpCapabilityunion (do not widen),:12-19scope fields,:26-40requireMcpCapabilityas the error templateClaudeAdapter.ts:3552,CursorAdapter.ts:547,GrokAdapter.ts:585,OpenCodeAdapter.ts:1221(gated by!server.externalat:1218),CodexAdapter.test.ts:579apps/server/src/provider/Layers/ClaudeAdapter.ts:3372-3378— thefull-accessshort-circuit that forces an in-handler gateapps/server/src/provider/CodexDeveloperInstructions.ts:7— the Codex-only tool advertisementSDK
@anthropic-ai/claude-agent-sdk@0.3.170sdk.d.ts:1433extraArgs?: Record<string, string | null>;:1933{ type: 'preset', preset: 'claude_code', append: '...' }Seam ledger —
docs/t3x/SEAMS.md: header totals (34 files, +1616/-187, merge-base64bf01619), the row-35 tripwire, "Reading the risk column", and thedocs/user/providers-claude.mdrow annotated as the one worth upstreaming.Churn figures in this issue were re-measured on 2026-08-02 with the ledger's own recipe:
MB=$(git merge-base main upstream/main); MBTS=$(git show -s --format=%ct $MB); git log --oneline --since="@$((MBTS-60*86400))" $MB -- <path> | wc -l.Duplicate search performed before filing
Fork (radroid/t3code): no duplicate, no overlap.
gh issue list --repo radroid/t3code --state all --limit 100returns exactly 24 issues (#4-#45). None concerns prompts, prompt storage, composer prefill, or settings-owned text. Closest by theme: #43 (a fork-owned MCP toolkit for orchestration) and #36 (voice input into the composer, closed NOT_PLANNED) — both touch machinery this feature reuses; neither is a duplicate. #42 and #43 are in fact unblocked by this feature'sT3xMcpToolkitsLivemount.Upstream (pingdotgg/t3code): no exact duplicate, but two open issues own roughly half each. The full 1623-issue title corpus was dumped (
gh issue list --state all --limit 6000 --json number,title,state,url) and grepped against 32 terms, yielding 65 candidates; ~35 additional body-levelgh search issuesqueries were run.Deltas that must appear in the issue body:
packages/effect-acp).~/.codex/prompts/*.md. Must be cited and differentiated (provider-agnostic, T3-owned storage, Settings-editable, model-editable). Caveat: not visible in the tree at merge-base64bf01619.The model-editable half has zero prior art in either repo. Empty result sets across ~35 body-level queries covering: prompt library, saved prompt, custom instructions, project instructions, prefill, composer default, output style, default prompt, meta prompt, prompt enhance, improve my prompt, starter prompt, project kickoff, onboarding prompt, app idea, self-improving, user memory, macro. No open PR upstream matches "prompt template" or "saved prompt snippet" either. That property — a model revising the user's own saved prompt under an explicit, default-OFF permission with diff, history and revert — is the genuinely novel part and should carry the issue's justification.
Contribution