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
Today, OpenCode plugins can react to events (via event hook) and intercept tool calls, but they have no structured way to maintain persistent context across a session. The current workarounds have serious limitations:
Subagent prompts: Injected into subagents only, not the main session.
experimental.chat.messages.transform: Can mutate messages but is too low-level; plugins must manually add context into system messages.
Tool-based context: Plugins can emit tool results, but these appear mid-conversation and don't establish upfront context for the turn.
Proposed Solution
Add three lifecycle hooks that give plugins a channel to inject context into the model's system prompt, with clear persistence semantics:
Hook
When it fires
Persistence
Use case
session.start
Session creation
Persistent across all turns
Load project state once (open tasks, git status, recent commits)
prompt.submit
Before each LLM turn
Ephemeral (this turn only)
Ephemeral retrieval for the current prompt (semantic search, relevant file summaries)
session.stop
After successful turn completion
Replaces persistent context
Update state based on turn outcome (extract facts, mark tasks complete, track progress)
Why three hooks?
session.start gives plugins a bootstrap point. Without this, a plugin must either 1) lazy-load context on first tool call (which is too late since the first prompt has already gone to the model), or 2) run expensive setup on every prompt.submit (wasteful). Issue #5409 identified this exact gap: loading dev context (git status, package.json, directory tree) currently requires manual work before each session.
prompt.submit provides an ephemeral channel. Persistent context from session.start eventually goes stale, so prompt.submit lets plugins inject fresh, query-specific context for this turn only. This is critical for retrieval-based plugins since semantic search results for the current user query should not persist into the next turn, but the search results should still be visible to the model.
session.stop closes the loop. After a turn completes, plugins often need to update their understanding of the world (for example, "the user just reviewed 3 files, mark them as seen"). Without a stop hook plugins must either 1) fire writes on every tool call (races, partial state), or 2) scan the entire conversation history on the next prompt.submit (expensive). session.stop fires once, with the turn outcome (text, toolCalls, finishReason), letting plugins atomically update persistent state.
How they work together
Session creation
→ session.start hook → sets persistent context ["Project: 5 open tasks"]
↓
User sends message
→ prompt.submit hook → ephemeral context ["Relevant: src/auth.ts"]
→ System prompt = [persistent] + [ephemeral] + [normal instructions]
↓
LLM turn completes
→ session.stop hook → updates persistent context ["Project: 4 open tasks"]
↓
Next user message
→ prompt.submit hook → new ephemeral context ["Relevant: src/user.ts"]
→ System prompt = [updated persistent] + [new ephemeral] + [normal instructions]
The persistent context survives compaction because it's stored on the session row, not in the message history. The ephemeral context is injected per-turn and does not bloat the session state.
Context budget and truncation
Plugins compete for the same context window. A lifecycle.maxInjectedContextChars config option (default 4000) caps total injected context per turn. If plugins exceed the limit, context is truncated with a logged warning. This prevents a misbehaving plugin from silently consuming the entire context window.
Claude Code parallel
Claude Code provides SessionStart, PreToolUse, PostToolUse, and Stop hooks. This proposal covers the same session-level lifecycle (SessionStart -> session.start, Stop -> session.stop) and adds an explicit prompt.submit hook for per-turn ephemeral context that Claude Code handles via PreToolUse/PostToolUse message injection. The goal is parity for plugins that need behavioral enforcement across a session's lifetime.
Concrete Use Cases
Context-aware project assistant:
session.start: Load open GitHub issues, recent commits, and active branch into persistent context
prompt.submit: Search embeddings of the codebase for files semantically related to the current user query, inject summaries
session.stop: Update a local "task state" file based on what the model did, persist a summary back into session.start context for next turn
Planning-with-files skill (as benchmarked in #17412):
session.start: Inject "You must maintain task_plan.md, findings.md, and progress.md"
prompt.submit: Load current state of the three planning files as ephemeral context
session.stop: Verify the three files were updated; if not, update persistent context with a reminder for the next turn
Why not existing mechanisms?
Mechanism
Limitation
How hooks fix it
AGENTS.md
Forgotten after compaction
Persistent context is stored outside message history
event hook
Fire-and-forget, can't inject context
Hooks mutate output that is explicitly injected into system prompt
Tool results
Appear mid-conversation, no upfront context
Context is prepended to system prompt before the turn
experimental.chat.messages.transform
Requires manual message splicing, fragile
First-class, typed, bounded channel
Implementation Sketch
Add plugin_context JSON column to session table
Add session.start hook at Session.create() — output context persisted to plugin_context
Add prompt.submit hook in SessionPrompt before handle.process() — output context prepended to system array for this turn only
Feature hasn't been suggested before.
Describe the enhancement you want to request
Related Issues
This builds on prior requests for session lifecycle hooks:
Problem
Today, OpenCode plugins can react to events (via
eventhook) and intercept tool calls, but they have no structured way to maintain persistent context across a session. The current workarounds have serious limitations:experimental.chat.messages.transform: Can mutate messages but is too low-level; plugins must manually add context into system messages.Proposed Solution
Add three lifecycle hooks that give plugins a channel to inject context into the model's system prompt, with clear persistence semantics:
session.startprompt.submitsession.stopWhy three hooks?
session.startgives plugins a bootstrap point. Without this, a plugin must either 1) lazy-load context on first tool call (which is too late since the first prompt has already gone to the model), or 2) run expensive setup on everyprompt.submit(wasteful). Issue #5409 identified this exact gap: loading dev context (git status, package.json, directory tree) currently requires manual work before each session.prompt.submitprovides an ephemeral channel. Persistent context fromsession.starteventually goes stale, soprompt.submitlets plugins inject fresh, query-specific context for this turn only. This is critical for retrieval-based plugins since semantic search results for the current user query should not persist into the next turn, but the search results should still be visible to the model.session.stopcloses the loop. After a turn completes, plugins often need to update their understanding of the world (for example, "the user just reviewed 3 files, mark them as seen"). Without a stop hook plugins must either 1) fire writes on every tool call (races, partial state), or 2) scan the entire conversation history on the nextprompt.submit(expensive).session.stopfires once, with the turn outcome (text,toolCalls,finishReason), letting plugins atomically update persistent state.How they work together
The persistent context survives compaction because it's stored on the session row, not in the message history. The ephemeral context is injected per-turn and does not bloat the session state.
Context budget and truncation
Plugins compete for the same context window. A
lifecycle.maxInjectedContextCharsconfig option (default 4000) caps total injected context per turn. If plugins exceed the limit, context is truncated with a logged warning. This prevents a misbehaving plugin from silently consuming the entire context window.Claude Code parallel
Claude Code provides
SessionStart,PreToolUse,PostToolUse, andStophooks. This proposal covers the same session-level lifecycle (SessionStart->session.start,Stop->session.stop) and adds an explicitprompt.submithook for per-turn ephemeral context that Claude Code handles viaPreToolUse/PostToolUsemessage injection. The goal is parity for plugins that need behavioral enforcement across a session's lifetime.Concrete Use Cases
Context-aware project assistant:
session.start: Load open GitHub issues, recent commits, and active branch into persistent contextprompt.submit: Search embeddings of the codebase for files semantically related to the current user query, inject summariessession.stop: Update a local "task state" file based on what the model did, persist a summary back intosession.startcontext for next turnPlanning-with-files skill (as benchmarked in #17412):
session.start: Inject "You must maintain task_plan.md, findings.md, and progress.md"prompt.submit: Load current state of the three planning files as ephemeral contextsession.stop: Verify the three files were updated; if not, update persistent context with a reminder for the next turnWhy not existing mechanisms?
eventhookexperimental.chat.messages.transformImplementation Sketch
plugin_contextJSON column tosessiontablesession.starthook atSession.create()— output context persisted toplugin_contextprompt.submithook inSessionPromptbeforehandle.process()— output context prepended tosystemarray for this turn onlysession.stophook after LLM loop exits normally — output context replacesplugin_contextlifecycle.maxInjectedContextCharsconfig optionlimitPluginContext()utility with truncation + warning logclearContextflag onsession.stopoutput for explicit wipeAll hooks are wrapped in
Effect.catchCause— failures are logged and swallowed, never blocking the session.