Repository navigation
[FEATURE]:Session lifecycle context hooks for persistent plugin state #28695
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
Related Issues
This builds on prior requests for session lifecycle hooks:
- [FEATURE]: SessionStart hook for session lifecycle events #5409 — SessionStart hook (startup/resume/compact triggers)
- [FEATURE]: Plugin hooks should be able to inject AI-visible messages into conversation context #17412 — Plugin hooks injecting AI-visible context
- [FEATURE]: Add expanded hook events to the plugin system to enable more powerful plugin capabilities. #21075 — Expanded hook events for more powerful plugins
- [FEATURE]: add session.stopping plugin hook to allow re-entering the agent loop #16626 — session.stopping hook to re-enter agent loop
Problem
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:
- AGENTS.md / SKILL.md instructions: Loaded once at the start and progressively forgotten as context grows. After compaction, behavioral rules are ~100% lost (as noted in [FEATURE]: Plugin hooks should be able to inject AI-visible messages into conversation context #17412).
- 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 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 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 contextsession.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_contextJSON column tosessiontable - Add
session.starthook atSession.create()— output context persisted toplugin_context - Add
prompt.submithook inSessionPromptbeforehandle.process()— output context prepended tosystemarray for this turn only - Add
session.stophook after LLM loop exits normally — output context replacesplugin_context - Add
lifecycle.maxInjectedContextCharsconfig option - Add
limitPluginContext()utility with truncation + warning log - Add
clearContextflag onsession.stopoutput for explicit wipe - Regenerate SDK types, add tests, add docs
All hooks are wrapped in Effect.catchCause — failures are logged and swallowed, never blocking the session.
Activity
github-actions commentedon May 21, 2026
This issue might be a duplicate of existing issues. Please check:
session.startlifecycle hook for loading project contextsession.stopping/session.stophook for re-entering or closing the agent loopprompt.submituse case)session.startandsession.endThis issue appears to be a unified proposal combining the above into a single design. The maintainers may prefer to consolidate discussion into the existing issues rather than track this separately. If the intent is to propose a specific combined design that supersedes or links all of the above, it would be helpful to clarify that in the issue body and cross-reference why a single unified proposal is preferable to the existing separate issues.
vuldin commentedon May 21, 2026
For more details and why I am interested specifically in these 3 hooks, implementing this would allow another project I work on to have feature parity when used with either Claude Code or OpenCode.
vuldin commentedon May 22, 2026
I have a branch that should close this issue, and I'll wait to hear any feedback here before creating the PR (following the guidelines).
Sanderhoff-alt commentedon Jul 6, 2026
I support this. A concrete use case is memory plugins that need reliable lifecycle hooks to persist session state.
session.idleworks as a per-run signal, but it does not provide a clear finalization point for flushing pending state when OpenCode is done with a main session or a task-created subagent session.ajdelaguila commentedon Jul 7, 2026
Full disclosure: I maintain Data Olympus, an MCP-served governed knowledge layer for coding agents.
This hook shape is almost exactly the surface a serious project-knowledge plugin would need. I would map it slightly differently from ordinary session memory:
session.start: inject only a compact bootstrap, for example "query this knowledge endpoint before architectural or policy decisions," plus workspace identity. Do not dump the knowledge base into the prompt.prompt.submit: run query-specific retrieval against the current user intent and inject only the few in-force rules or decisions that govern this turn.session.stop: extract candidate learnings or rule changes from the turn, but write them as reviewable proposals rather than silently promoting them into durable memory.The last point matters. For coding agents, there is a difference between "the session observed X" and "X is now an authoritative project rule." We have had good results making the durable layer git-native and reviewed: markdown concepts with
status,tier,type, supersession chains, and an MCP write path where agents can propose edits but the system can hold low-confidence changes for human review.So the lifecycle hooks here would let an OpenCode plugin keep the model aligned without relying on AGENTS.md surviving compaction, while also avoiding the opposite failure mode: every session note becoming permanent policy.
Reference if useful: https://github.com/knaisoma/data-olympus
github-actions commentedon Sep 6, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.