Snippets
Hashtag-based snippet expansion plugin for OpenCode - instant inline text shortcuts
84
+3 in 30 days
2,194
691 in 7 days
59.6
Multi-signal model
5 hours ago
2026-10-05
Install and configure
opencode.jsonWrites 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"]
}Writes to ~/.config/opencode/opencode.json — applies to every project.
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-snippets@3.3.1"]
}If you want to modify the plugin locally, install it into the project and reference the local path.
shell
pnpm add -D opencode-snippetsOpenCode 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
#hashtagaway
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.
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
VISUALorEDITORand 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 #nameindicator 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$ cmdplus 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
Better Compact
better-compact
OpenCode plugin that improves long-session context with boundary-time pruning
Tell Sessions
opencode-tell-sessions
Inter-session direct messaging (DM) for OpenCode V1 & V2: agents in different sessions can message each other
Tps Meter
opencode-tps-meter
Live tokens-per-second meter for OpenCode (v1 and v2)