Skip to content

[FEATURE]: SessionStart hook for session lifecycle events #5409

Closed as not planned

Description

@simonwjackson

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Request

Add a SessionStart hook (similar to Claude Code) that runs at key session lifecycle points.

Demo

POC: https://github.com/simonwjackson/opencode/pull/new/feature/session-start-hook

Made with VHS

Use Cases

  • Load development context (existing issues, recent codebase changes)
  • Install dependencies or run setup scripts
  • Initialize project-specific tooling
  • Run validation checks before a session begins

Motivation

I have a custom tool that gathers project context (git status, package.json, tsconfig, directory tree) to augment AGENTS.md.

The problem: Manually calling it adds ~12 seconds before I can start working.

What I want: This should run automatically in the background before the first message is sent. Subagents would load this automatically as well.

Why This Can't Be Done With Plugins Today

I investigated the current plugin system and found:

What works:

  • Plugin init function runs once at OpenCode bootstrap (approximates startup)
  • event hook can react to session.created and session.compacted

What's missing:

  • resume - No event fires when resuming via --continue/--session (it just navigates to an existing session)
  • No way to distinguish why a session started

The core limitation is that resume is a UI navigation operation that doesn't fire a bus event.

Proposed Solution

A new plugin hook that fires at session boundaries:

"session.start"?: (input: {
  /** What triggered this hook */
  trigger: "startup" | "resume" | "compact"
  /** Session ID (when resuming an existing session) */
  sessionID?: string
}) => Promise<void>

Triggers

Trigger When it fires
startup Fresh OpenCode start
resume --resume, --continue, or /resume
compact Auto or manual compaction

Example

  export const TestSessionStartPlugin = async ({ $ }) => ({
    "session.start": async (input, output) => {
      const gitBranch = await $`git branch --show-current 2>/dev/null`.text().catch(() => "unknown")                                                                                                                                            
      const gitStatus = await $`git status --short 2>/dev/null`.text().catch(() => "")                                                                                                                                                          
      const recentCommits = await $`git log --oneline -3 2>/dev/null`.text().catch(() => "")                                                                                                                                                    
                                                                                                                                                                                                                                                
      output.additionalContext = `<project-context>                                                                                                                                                                                             
  ## Current Branch: ${gitBranch.trim()}                                                                                                                                                                                                        
                                                                                                                                                                                                                                                
  ## Uncommitted Changes                                                                                                                                                                                                                        
  ${gitStatus.trim() || '(none)'}                                                                                                                                                                                                               
                                                                                                                                                                                                                                                
  ## Recent Commits                                                                                                                                                                                                                             
  ${recentCommits.trim() || '(none)'}                                                                                                                                                                                                           
  </project-context>`                                                                                                                                                                                                                           
    }                                                                                                                                                                                                                                           
  })   

References

Activity

  1. added
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    on Dec 11, 2025
  2. github-actions commented on Dec 11, 2025

    @github-actions
    Contributor

    This feature request appears to be unique and doesn't duplicate existing issues. However, it's related to several ongoing plugin system improvements:

    These issues are complementary to your SessionStart hook proposal and might benefit from coordinated implementation.

  3. Brandtweary commented on Dec 14, 2025

    @Brandtweary

    Tried the POC branch - the session.start hook works for startup (new sessions) but fails on resume when using --continue. Getting "No context found for instance" error because Plugin.triggerSessionStart is called during route navigation before the plugin context is fully initialized. The hook fires (confirmed via logging) but the plugin system isn't ready yet at that point in the --continue bootstrap sequence.

  4. arawak commented on Mar 17, 2026

    @arawak

    I implemented this on my branch and am opening a PR for it.

    My main motivation was using the context-mode plugin with OpenCode so it can restore one-shot session context automatically instead of requiring manual setup each time.

    The implementation adds session.start with startup, resume, and compact triggers, stores pending hook context on the session, and injects it into the next LLM turn. I also added tests for the core hook behavior, resume wiring, and the fork startup timing edge case.

    One tradeoff in the current implementation: pending context is consumed lazily on the next model turn, so resume-triggered context can become stale if a session is resumed but not prompted soon after. I documented that in code/comments rather than broadening the scope of this PR.

  5. c0dn commented on Apr 4, 2026

    @c0dn

    Just wanted to add some context from a real use case — I'm building opencode-cache-guard, a plugin that warns when Anthropic prompt cache has expired on a stale session. The gap this issue describes is exactly what's blocking the ideal UX (warn on session load rather than on send).

    #18007 looks like a solid implementation of this. It's drifted from dev due to the Effect migration but the conflicts seem mechanical. Happy to help with a rebase if that's useful.

    ps: sorry for the tag, won't happen again

  6. removed
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    on May 3, 2026
  7. guillempuche commented on Jun 20, 2026

    @guillempuche

    Use case: session.created runs a script that provisions a git worktree's Postgres database and MinIO bucket if they don't exist yet — works well for new sessions. The gap is --continue: resuming an existing session fires nothing, so there's no place to print a "worktree already provisioned, run pnpm dev" reminder on resume. A session.resumed event or a type: "new" | "resumed" field on session.created would close this.

    Reference: worktree-up.sh

  8. cassiodias commented on Jun 28, 2026

    @cassiodias

    +1 on a native session.start hook with a source matcher. Adding a data point from running the plugin workaround in production.

    I ship a session.created plugin that auto-bootstraps session context (runs a setup script, surfaces a short status toast, injects a one-line primer for the model). It works well for fresh starts, and a couple of findings that might help scope this:

    • session.created fires on the first real interaction, not at TUI launch. Bootstrapping on launch alone misses it; subscribing to session.created is the right trigger.
    • The factory can evaluate more than once per process (e.g. a plugin present in more than one loaded config layer), so the handler can see session.created twice for one new session. A module-level Set of seen session ids keyed on event.properties.info.id dedupes it; without that guard, side-effecting bootstrap runs twice. A native hook with a documented at-most-once contract would remove this footgun.
    • The resume blind spot is the real gap, exactly as described above. --continue / --resume / /resume produce no bus event, so a resumed session silently skips bootstrap and the context that startup loaded is absent for the rest of that session. There is no plugin-side workaround today -- session.updated is too noisy and carries no "this is a resume" signal. This is the one case the plugin pattern genuinely cannot cover.

    For the matcher, the three sources in the proposal (startup / resume / compact) map cleanly onto how the lifecycle already behaves -- compact overlaps with the existing session.compacted event, and startup overlaps with session.created, so the net-new capability the native hook unlocks is really resume. Even just emitting a bus event on resume (without the full session.start hook) would close the gap for plugin authors.

    Generic skeleton showing the session.created handler + the at-most-once dedupe guard: https://gist.github.com/cassiodias/83a56ad40e1add6b95ab9b7dc9d99963

  9. github-actions commented on Aug 28, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

  10. added a commit that references this issue on Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions