Skip to content
    ↑↓ select↵ openesc close
    JosXa

    Snippets

    opencode-snippets·v3.3.1·Other

    Hashtag-based snippet expansion plugin for OpenCode - instant inline text shortcuts

    GitHub stars

    84

    +3 in 30 days

    Monthly installs

    2,194

    691 in 7 days

    Composite score

    59.6

    Multi-signal model

    Last commit

    5 hours ago

    2026-10-05

    Install and configure

    opencode.json

    Writes to this project's opencode.json — applies to this repository only.

    opencode.json

    {
      "$schema": "https://opencode.ai/config.json",
      "plugin": ["opencode-snippets@3.3.1"]
    }

    OpenCode loads npm dependencies through its embedded runtime on startup and caches them locally — no manual global install needed.

    ✨ Instant inline text expansion for OpenCode - Type #snippet anywhere in your message and watch it transform.

    [!TIP] Share Your Snippets!
    Got a snippet that saves you time? Share yours or steal ideas from the community! Browse and contribute in GitHub Discussions.

    Why Snippets?

    As developers, we DRY (Don't Repeat Yourself) our code. We extract functions, create libraries, compose modules. Why should our prompts be any different?

    Stop copy-pasting (or worse, typing 🤢) the same instructions into every message. Snippets bring software engineering principles to prompt engineering:

    • 🔄 DRY - Write once, reuse everywhere
    • 🧩 Composability - Build complex prompts from simple pieces
    • 🔧 Maintainability - Update once, apply everywhere
    • 🔍 Discoverability - Your team's best practices, always a #hashtag away

    OpenCode's /slash commands must come first. Snippets work anywhere:

    # Slash commands (must be first):
    /git-status Please review my changes
    
    # Snippets (anywhere!):
    Please review my changes #git-status and suggest improvements #code-style
    

    Snippets work like @file mentions - natural, inline, composable.

    🎯 Composable by Design

    Snippets compose with each other and with slash commands. Reference #snippets anywhere - in your messages, in slash commands, even inside other snippets:

    Example: Extending commands with snippets

    ~/.config/opencode/command/commit-and-push.md:

    ---
    description: Create a git commit and push to remote
    ---
    Please create a git commit with the current changes and push to the remote repository.
    #use-conventional-commits
    
    Here is the current git status:
    !`git status`
    
    Here are the staged changes:
    !`git diff --cached`
    
    #project-context
    

    You could also make "current git status and staged changes" a shell-enabled snippet of its own.

    Example: Snippets composing snippets

    ~/.config/opencode/snippet/code-standards.md:

    #style-guide
    #error-handling
    #testing-requirements
    

    https://github.com/user-attachments/assets/76975a9e-e326-431e-8be5-39a9f6572851

    ~/.config/opencode/snippet/full-review.md:

    #code-standards
    #security-checklist
    #performance-tips
    

    Compose base snippets into higher-level ones. Type #full-review to inject all standards at once, keeping each concern in its own maintainable file.

    The power: Mix and match. Type #tdd #careful for test-driven development with extra caution. Build /commit #conventional-commits #project-context for context-aware commits. Create layered prompts from small, reusable pieces.
    image

    Installation

    Add "opencode-snippets" to plugins in opencode.json. OpenCode loads the terminal plugin automatically, including autocomplete, field forms, and the library.

    {
      "plugins": [
        "opencode-snippets"
      ]
    }
    
    Local Development

    Run bun install and bun run build. For local testing, add the built dist directory to plugins in both opencode.json and cli.json:

    {
      "plugins": [
        "file:///absolute/path/to/opencode-snippets/dist"
      ]
    }
    

    Rebuild after source changes to test the updated server and terminal entrypoints.

    Quick Start

    1. Open /snippets, choose New, name it careful, and select Global.

    2. Enter the source below and press Ctrl+S to save:

    ---
    aliases: safe
    ---
    Think step by step. Double-check your work before committing changes.
    Ask clarifying questions if anything is ambiguous.
    

    3. Use it anywhere:

    Type #careful or its alias #safe in a message.

    https://github.com/user-attachments/assets/ebb303b5-d41b-4d87-8f08-eb1d730db5c8

    Where to Store Snippets

    The global config root is OPENCODE_CONFIG_DIR when set, otherwise $XDG_CONFIG_HOME/opencode, otherwise ~/.config/opencode. The paths below show the default root. Snippet files, snippet/config.jsonc, and plugin logs all follow the selected root. The server and TUI resolve it in their own process environment; when attaching to another machine, keep the snippet collections in those roots in sync. An explicit globalDirectory plugin option still overrides the snippet directory.

    Snippets can be global (~/.config/opencode/snippet/*.md or ~/.config/opencode/snippets/*.md) or project-specific (.opencode/snippet/*.md or .opencode/snippets/*.md). Both singular and plural directory names are loaded automatically. Project snippets override global ones with the same name, and snippet/ wins over snippets/ within the same scope.

    Project snippet directories are resolved against the canonical project root. A symlinked project snippet directory, including one that points outside the project, is rejected rather than loaded or modified.

    Processing state

    New submissions read the current snippet files, including nested references and completed drafts. Changes saved in the library or another editor apply to the next submission. Replayed messages retain their original expansion and skill content.

    If snippet processing fails, the plugin preserves the original message and logs a warning. Invalid arguments, templates, or old snippet references must not block new conversation turns. Validation completes before shell commands run; a validation failure discards the message's planned expansions and effects. Correct the snippet or its arguments and submit it again to expand it. Snippet failures in skill tool results also preserve the original result.

    The plugin records a bounded amount of processing output so rebuilt request history and server restarts cannot repeat shell side effects. State lives under the user's private data directory ($XDG_DATA_HOME/opencode/opencode-snippets/v2, or ~/.local/share/opencode/opencode-snippets/v2) rather than inside a project. Directories use mode 0700 and records use 0600.

    Only hashed project/message identifiers, completion status, timestamps, and output needed for replay are retained; raw input prompts and project paths are not stored. Records older than 30 days are pruned, with additional limits of 100 sessions per project and 1,000 messages per session. Deleting an OpenCode session removes that session's records immediately.

    Features

    Aliases

    Define multiple triggers for the same snippet:

    ~/.config/opencode/snippet/cherry-pick.md:

    ---
    aliases:
      - cp
      - pick
    description: "Git cherry-pick helper"
    ---
    Always pick parent 1 for merge commits.
    

    Now #cherry-pick, #cp, and #pick all expand to the same content.

    Single alias doesn't need array syntax:

    ---
    aliases: safe
    ---
    

    You can also use JSON array style: aliases: ["cp", "pick"]

    Fields and forms

    Snippets declare typed inputs in YAML frontmatter and reference answers with ordinary Handlebars variables:

    ---
    fields:
      app:
        label: App name
        required: true
    ---
    MyApps (search for {{app}})
    

    Accept a completion or type a space after an exact snippet name to fill its form. Confirming writes a readable reference such as #myapps(app="Payroll") into the composer; sending the message expands it. Cancel preserves the original reference. Use Edit snippet fields for the invocation under the cursor to change answers.

    Autocomplete marks snippets that open forms with ☷ after their name. Forms support text, multiline text, numbers, checkboxes, and single or multiple selection lists, with defaults and validation. The action row contains OK, Cancel, and Help. A short keyboard hint is always visible; Help reveals the full key list. Tab/Shift+Tab move between fields and buttons, arrows select, Space toggles a checkbox or multiselect choice, Ctrl+J inserts a newline, Enter confirms, and Escape cancels. Clicking a multiselect choice toggles it.

    ---
    fields:
      count:
        label: Options
        type: number
        default: 5
        min: 0
        integer: true
    ---
    {{#if (gt count 0)}}give me {{count}} {{plural count "option" "options"}} to choose from{{/if}}
    

    Metadata does not print answers; body variables and conditions control the output. Conditions use typed answers; this example emits nothing for zero and uses singular wording for one. For a preset, put #options(count=10) in ten-options.md. An explicit outer answer such as #ten-options(count=2) overrides that default. Nested snippets share their parent's answers; separate invocations remain independent.

    The fields mapping determines input order and opts the body into Handlebars. Use fields: {} to render a body with no inputs of its own. Body references do not declare fields, and legacy snippets without their own mapping or an inline skill helper retain literal placeholders. Select options use YAML arrays, such as options: [quick, normal, thorough].

    Use type: multiselect to select several options. Its default is an array of selected strings. List every option there to start with all choices selected. Without a default, nothing is selected. required: true requires at least one choice. {{checks}} prints the selections separated by commas. Use {{#each checks}}- {{this}}{{/each}} to render them individually. The audit-tests example starts with all checks selected.

    Arguments use named keys, JSON-quoted text, finite numbers, yes/no booleans, and arrays of JSON-quoted strings, such as #audit-tests(checks=["No meaningful assertions", "Assertions that cannot fail"]). An explicit checks=[] clears all selections. Answers stay literal even when they contain hashtags, shell commands, or template syntax. Headless invocations use defaults and reject missing required answers before effects run.

    See field authoring and natural-language examples for all field types, constraints, escaping, repeated values, and conditional prose. The example templates include review, reword, and options presets. Review emits no directive for zero reviewers or zero cycles; one reviewer omits parallelism and one cycle omits repetition. Reword and options also omit their requests for zero.

    For inline skill content in a snippet, use {{skill "review"}}. Existing XML skill tags remain supported; #skill(review) continues to load hidden context with a visible marker.

    Shell Command Substitution

    The plugin adds shell substitution to regular OpenCode prompts, not just snippet files. Use !`command` for output-only injection and !>`command` when you want the executed command shown too:

    Current branch: !`git branch --show-current`
    Last commit: !`git log -1 --oneline`
    Working directory: !`pwd`
    Debug listing: !>`ls`
    

    Default: !`ls` injects only command output, matching OpenCode command templates.

    Verbose form: !>`ls` →

    $ ls
    --> <output>
    

    LLMs tend to trust the output more when they can see which terminal command just ran. The command gives the output context, which makes it more informative and easier to interpret.

    Recursive Includes

    Snippets can include other snippets using #snippet-name syntax. This allows building complex, composable snippets from smaller pieces:

    # In base-style.md:
    Use TypeScript strict mode. Always add JSDoc comments.
    
    # In python-style.md:
    Use type hints. Follow PEP 8.
    
    # In review.md:
    Review this code carefully:
    #base-style
    #python-style
    #security-checklist
    

    Loop Protection: Snippets are expanded up to 15 times per message to support deep nesting. If a circular reference is detected (e.g., #a includes #b which includes #a), expansion stops after 15 iterations and the remaining hashtag is left as-is. A warning is logged to help debug the issue.

    Example of loop protection:

    # self.md contains: "I reference #self"
    # Expanding #self produces:
    I reference I reference I reference ... (15 times) ... I reference #self
    

    This generous limit supports complex snippet hierarchies while preventing infinite loops.

    Prepend and Append Blocks

    For long reference material that would break your writing flow, use <append> blocks to place content at the end of your message:

    ---
    aliases: jira-mcp
    ---
    Jira MCP server
    <append>
    ## Jira MCP Usage
    
    Use these custom field mappings when creating issues:
    - customfield_16570 => Acceptance Criteria
    - customfield_11401 => Team
    </append>
    

    Input: Create a bug ticket in #jira-mcp about the memory leak

    Output:

    Create a bug ticket in Jira MCP server about the memory leak
    
    ## Jira MCP Usage
    
    Use these custom field mappings when creating issues:
    - customfield_16570 => Acceptance Criteria
    - customfield_11401 => Team
    

    Write naturally—reference what you need mid-sentence—and the context follows at the bottom.

    Use <prepend> for content that should appear at the top of your message. Multiple blocks of the same type are concatenated in order of appearance.

    Block behavior:

    • Content outside <prepend>/<append> blocks replaces the hashtag inline
    • If a snippet has only blocks (no inline content), the hashtag is simply removed
    • Blocks from nested snippets are collected and assembled in the final message
    • Unclosed tags are handled leniently (rest of content becomes the block)
    • Nested blocks are not allowed—the hashtag is left unchanged

    Inject Blocks (Experimental)

    Add persistent context that the LLM sees throughout the entire agentic loop, without cluttering your visible message:

    ---
    aliases: safe
    ---
    Think step by step.
    <inject>
    IMPORTANT: Double-check all code for security vulnerabilities.
    Always suggest tests for any implementation.
    </inject>
    

    Input: Review this code #safe

    What happens:

    • Your message shows: Review this code Think step by step.
    • The LLM also receives the inject content as a separate context message
    • This context persists for the entire conversation turn (agentic loop)

    Use inject blocks for rules, constraints, or instructions that should influence all LLM responses without appearing inline in your message.

    Injected context is placed N messages from the bottom of the conversation (default: 5) to prevent instruction overfitting, where the model fixates on injected content as if it were the user's latest directive. As the conversation grows, the injection floats upward, maintaining a steady distance from the latest turn. Configure the offset with injectRecencyMessages. For the full design rationale, see Injection Placement Strategy.

    Enable in config:

    {
      "experimental": {
        "injectBlocks": true
      }
    }
    

    Skill Rendering (Experimental)

    Inline OpenCode skills directly into your messages using XML-style tags:

    Create a Jira ticket. <skill>jira</skill>
    

    Or use the self-closing format:

    <skill name="jira" /> Create a ticket for the bug.
    

    Enable in config:

    {
      "experimental": {
        "skillRendering": true
      }
    }
    

    The plugin uses OpenCode's native skill registry for both expansion and autocomplete. This includes skills discovered by OpenCode, configured skill sources, and skills supplied by plugins. Skill IDs and display names are accepted; autocomplete inserts the native ID to distinguish skills with the same display name.

    When a skill tag is found, it's replaced with the skill's content body (frontmatter stripped). Unknown skills leave the tag unchanged.

    Skill Loading (Experimental)

    Load a skill with OpenCode-style wrapper content without showing the full skill body inline:

    Write this in caveman mode. #skill(caveman)
    

    Quoted names are also supported:

    #skill("opencode-config")
    

    Enable in config:

    {
      "experimental": {
        "skillLoading": true
      }
    }
    

    When enabled, the user-visible message shows ↳ Loaded name, while the model receives an injected OpenCode-style <skill_content> payload immediately after that message. Multiple #skill(...) calls in one message are injected in their final visible order, including loads introduced by recursive snippets and prepend/append blocks.

    XML skill tags render before hashtag expansion; #skill(...) loads resolve after recursive hashtag expansion; shell substitutions run last. Loaded skill bodies also expand snippet hashtags and shell substitutions. Within <inject> blocks, only hashtag references expand. Skill-tool results expand XML tags and recursive hashtags using the configured injection flag.

    Quick project-local demo in this repo:

    Explain closures in two lines. #skill(demo-voice)
    

    Or use the included snippet that expands into #skill(...):

    #demo-skill Explain closures in two lines.
    

    Demo files live at .opencode/skill/demo-voice/SKILL.md and .opencode/snippet/demo-skill.md.

    Snippet library

    Open /snippets to manage your project and global snippets.

    • Enter edits the Markdown source; Ctrl+S saves.
    • Shift+Enter opens the file in VISUAL or EDITOR and reloads it on return.
    • More or : opens actions: new, reload, duplicate, rename, move, delete, copy reference, and form testing.
    • Escape steps back; q closes from navigation and keeps drafts for reopening.

    The list and preview use Vim navigation. F1 shows the key list.

    Keys Navigation
    j / k, arrows, Ctrl+N / Ctrl+P Next / previous row; stop at the ends
    gg / G, Home / End First / last row or preview line
    3j, 5G, 5gg Repeat a motion or go to a numbered row or line
    H / M / L Select the top / middle / bottom visible list row
    Ctrl+D / Ctrl+U Half page down / up
    Ctrl+F / Ctrl+B, PageDown / PageUp Full page down / up
    Ctrl+E / Ctrl+Y Scroll down / up one line
    zt / zz / zb Align the selected list row at the top / middle / bottom, where content permits
    h / l, Left / Right Focus the list / preview
    Ctrl+W, then h / k or l / j Focus the list or the detail pane
    Ctrl+W, then w Switch panes
    / / ?, then text and Enter Filter forward / backward by name, alias, or description
    n / N Next / previous filtered match, following / reversing the search direction; wraps
    Ctrl+O / Ctrl+I Back / forward through reference, search, and boundary jumps; clears filters
    Tab / Shift+Tab Visit every control; j / k also move between actions
    i / Enter Open the selected source editor

    Counts and incomplete key sequences appear in the footer; Escape cancels them. Navigation shortcuts apply while the list, preview, or an action has focus. Search fields accept ordinary text, and the source editor keeps its own keys. The detail pane contains the editor while editing. Terminals that encode Ctrl+I as Tab use it for focus traversal. Distinct Ctrl+I requires extended keyboard support.

    In the composer, Ctrl+G edits the fields of the invocation under the cursor.

    Example Snippets

    ~/.config/opencode/snippet/context.md

    ---
    aliases: ctx
    ---
    Project: !`basename $(pwd)`
    Branch: !`git branch --show-current`
    Recent changes: !`git diff --stat HEAD~3 | tail -5`
    

    ~/.config/opencode/snippet/minimal.md

    ---
    aliases:
      - min
      - terse
    ---
    Be extremely concise. No explanations unless asked.
    

    Snippets vs Slash Commands

    Feature /commands #snippets
    Position Must come first 🏁 Anywhere 📍
    Multiple per message No ❌ Yes ✅
    Live shell data Yes 💻 Yes 💻
    Best for Triggering actions & workflows ⚡ Context injection 📝

    [!TIP]

    My recommendation:

    Use /slash commands for triggering actions and workflows imperatively - anything that needs to happen right now: /commit-and-push, /add-worktree, or /pull-rebase.
    Use #snippets for all other context engineering.

    If you can't decide, get the best of both worlds and just have your command proxy through to the snippet:

    ~/.config/opencode/command/pull.md:

    ---
    description: Proxy through to the snippet at snippet/pull.md
    ---
    #pull
    

    Configuration

    The plugin can be configured via config.jsonc files:

    • Global: ~/.config/opencode/snippet/config.jsonc
    • Project: .opencode/snippet/config.jsonc (overrides global settings)

    Snippet markdown files are loaded from both snippet/ and snippets/, but config files stay in snippet/config.jsonc.

    A default config file is created automatically on first run.

    Full Configuration Example

    {
      "$schema": "https://raw.githubusercontent.com/JosXa/opencode-snippets/v3.3.1/schema/config.schema.json",
      "logging": {
        "debug": false // Enable debug logging (logs: $XDG_DATA_HOME/opencode/log/snippets/daily/)
      },
      "experimental": {
        "injectBlocks": false, // Enable <inject>...</inject> blocks for persistent context
        "skillRendering": false, // Enable <skill>name</skill> tag expansion
        "skillLoading": false // Enable #skill(name) OpenCode-style loading
      },
      "injectRecencyMessages": 5 // How many messages from the bottom to place injected context
    }
    

    All boolean settings accept: true, false, "enabled", "disabled"

    Debug Logging

    Logs are written to $XDG_DATA_HOME/opencode/log/snippets/daily/ (or ~/.local/share/opencode/log/snippets/daily/ when XDG_DATA_HOME is unset). Info, warning, and error messages are always written; logging.debug also enables debug messages. Keeping logs outside the OpenCode configuration directory prevents each append from reloading config and rebuilding skill watches.

    Behavior Notes

    • Snippets expand everywhere: regular chat, question responses, skills, and slash commands
    • Injected snippet context is placed N messages from the bottom (configured by injectRecencyMessages) and shows a ↳ Injected #name indicator when first registered
    • #skill(name) inserts OpenCode-style skill payload text above the visible user message while keeping the transcript inline placeholder compact
    • Snippets are loaded once at plugin startup
    • Hashtag matching is case-insensitive (#Hello = #hello)
    • Unknown hashtags are left unchanged
    • !`cmd` injects output only, while !>`cmd` injects $ cmd plus output
    • Failed shell commands preserve the original syntax in output
    • Frontmatter is stripped from expanded content
    • Only user messages are processed (not assistant responses)

    Contributing

    Runtime tests for OpenCode 2

    Run the unit and renderer tests with bun test. To exercise the built plugin in real OpenCode 2 processes, install the CLI version matching @opencode/plugin in package.json, then run:

    bun run test:v2
    

    Set OPENCODE2_BIN to test a specific CLI executable. The runtime suite uses isolated home/config/data directories and a local deterministic model endpoint. It verifies saved messages and actual model requests across CLI submissions, native attachments, skill-tool calls, queued input, commands, restarts, forks, and project boundaries. Failed tests retain their temporary directory and print its path, including provider requests, session exports, and process logs.

    The CLI and native prompt API have separate coverage because run serializes multiword arguments with quotes and inlines text supplied by --file; native API files remain attachments. Update these explicit CLI compatibility assertions when the host changes that behavior.

    Contributions welcome! Please open an issue or PR on GitHub. 👥 Discord Forum

    License

    MIT

    Similar plugins