Go Extensions
Extensions let you add new behaviors to sharur beyond what’s possible with skills and prompt templates. They can observe and modify every stage of the agent loop — from the raw user input through each LLM turn and tool call to compaction and session teardown. Extensions run as separate processes and communicate with sharur via gRPC.
Extension Types
| Type | Language | Use Case |
|---|---|---|
| Go binary | Go | High-performance tools, direct filesystem access |
| Python script | Python | Data processing, ML integrations, API calls |
| Any executable | Any | Shell scripts, compiled binaries from any language |
All extension types use the same gRPC protocol. The loader treats .py files specially (runs them with the configured Python interpreter), and everything else is executed directly as a binary.
Extension Discovery
Extensions are loaded from directories listed in your config under extensions:
Or globally in ~/.sharur/config.json.
Place your extension binary or script in the configured directory. sharur will automatically discover and launch it on startup.
You can also load a specific extension at runtime with the --extension flag:
The Plugin Interface
Every Go extension implements the extensions.Plugin interface from github.com/goppydae/sharur/extensions. Embed extensions.NoopPlugin and override only the hooks you need.
Load-time hooks
| Method | When called | Purpose |
|---|---|---|
Name() | On load | Returns the extension’s identifier string |
Tools() | On load | Returns tool definitions the agent can call |
ExecuteTool() | On tool call | Executes a tool registered by this extension |
Session lifecycle hooks
| Method | When called | Purpose |
|---|---|---|
SessionStart(ctx, sessionID, reason) | Session attached or first prompt | Open connections, initialize per-session state |
SessionEnd(ctx, sessionID, reason) | Session reset | Flush buffers, close connections |
reason is "new" for a fresh session and "resume" for one loaded from disk.
Agent loop hooks
| Method | When called | Purpose |
|---|---|---|
AgentStart(ctx) | User prompt received, loop begins | Per-prompt setup, logging |
AgentEnd(ctx) | Agent loop completes | Per-prompt teardown, emit metrics |
TurnStart(ctx) | Start of each LLM request turn | Per-turn timing |
TurnEnd(ctx) | After each turn’s tool calls finish | Per-turn cleanup |
Transformation hooks
| Method | When called | Can modify | Purpose |
|---|---|---|---|
ModifyInput(ctx, text) | Before user text hits the transcript | Yes — transform or consume | Pre-process input, implement shortcuts |
ModifySystemPrompt(prompt) | Before each LLM request | Yes — returns new prompt | Inject dynamic context into the system prompt |
BeforePrompt(ctx, state) | Before each LLM request | Yes — returns new state | Change model, provider, or thinking level |
ModifyContext(ctx, messagesJSON) | Before each LLM request is built | Yes — returns new JSON | Filter or inject messages sent to the LLM (transcript unchanged) |
BeforeProviderRequest(ctx, requestJSON) | Just before the request is sent | Yes — returns new JSON | Modify temperature, max tokens, tools list |
AfterProviderResponse(ctx, content, numToolCalls) | After LLM stream consumed | No | Observe response text and tool call count |
BeforeToolCall(ctx, call, args) | Before each tool execution | Yes — can intercept | Block or replace tool execution |
AfterToolCall(ctx, call, result) | After each tool execution | Yes — returns new result | Observe or modify tool results |
BeforeCompact(ctx, prep) | Before LLM-based summarization | Yes — can skip | Provide a custom compaction summary |
AfterCompact(ctx, freedTokens) | After compaction completes | No | Observe freed token count |
Key behaviors:
ModifyInputreturnsagent.InputResult. SetActionto"continue"(pass through unchanged),"transform"(use theTextfield instead), or"handled"(consume the message entirely — it is not appended to the transcript and the agent does not run).ModifyContextandBeforeProviderRequestwork with JSON strings at the gRPC boundary. TheGRPCClientmarshals/unmarshals the Go structs automatically.BeforeCompactreturns""(empty) to let the default LLM summarization run, or a non-empty summary string to provide your own and skip the LLM call. Theprepargument includes the message count, estimated token count, and the previous summary (if any).BeforeToolCallreturns(ToolResult, true)to intercept (the tool does not execute), or(ToolResult{}, false)to allow normal execution.
Example: Git Context Injection
Build and auto-discover:
Example: Session Lifecycle Hooks
Example: Input Transformation
ModifyInput runs before the user text is added to the transcript. Return "handled" to consume shortcuts silently, or "transform" to rewrite the text:
Example: Custom Compaction
Return a non-nil *agent.CompactionResult from BeforeCompact to supply your own summary and bypass the default LLM-based summarization:
Example: Extension with Custom Tools
Extensions can contribute tools the agent calls just like built-in tools:
Example: Intercepting Tool Calls (Sandbox)
BeforeToolCall lets you block or replace any built-in tool call:
See examples/sandbox/ for a complete standalone implementation.
Extension Lifecycle
flowchart TD
Start["shr startup"] --> Scan["Scan extension directories"]
Scan --> Launch["Launch subprocess
SHARUR_SOCKET_PATH=..."]
Launch --> Socket["Wait for socket · dial gRPC"]
Socket --> Init["Name() · Tools()"]
Init --> SS["SessionStart(sessionID, reason)
on new session or resume"]
SS --> MI["ModifyInput(text)"]
MI --> AS["AgentStart()"]
subgraph turn ["Per LLM turn (repeats until no tool calls)"]
direction TB
T1["BeforePrompt() · ModifySystemPrompt()
ModifyContext() · BeforeProviderRequest()"]
T2[/"LLM streams"/]
T3["AfterProviderResponse() · TurnStart()"]
subgraph toolloop ["Per tool call"]
BTC["BeforeToolCall()"] --> Intercept{"intercept?"}
Intercept -->|yes| CustomResult["return custom ToolResult"]
Intercept -->|no| Exec["execTool() · AfterToolCall()"]
end
TE["TurnEnd()"]
T1 --> T2 --> T3 --> toolloop --> TE
end
AS --> turn
turn --> AE["AgentEnd()"]
subgraph compact ["On compaction (auto or /compact)"]
direction TB
BC["BeforeCompact(prep)"] --> CustomSummary{"return non-nil?"}
CustomSummary -->|yes| SkipLLM["skip LLM summarization"]
CustomSummary -->|no| LLMSum["LLM summarizes"]
SkipLLM --> AC["AfterCompact(freedTokens)"]
LLMSum --> AC
end
AE --> SE["SessionEnd(sessionID, reason)
on session reset"]
SE --> Shutdown["shr shutdown · kill subprocess"]In-Process Go Extension (Advanced)
If your extension is written in Go and you control the build, you can implement agent.Extension directly via the SDK and register it without the gRPC overhead:
Pass the extension via ag.SetExtensions() from the SDK or directly in cmd/shr.
Tips
- Extensions are isolated processes. A crash in an extension will not crash
sharur— the loader catches errors and logs them. - Keep
BeforePromptandModifySystemPromptfast. They run before every single LLM call. Cache data when possible; avoid blocking network calls. ModifyContextdoes not affect the stored transcript. Changes to the message slice are only visible to the LLM for that turn.- Use skills for static context. If you only need to append static text to the system prompt, a skill is simpler than an extension.
- Extensions are global. All extensions in the configured directories are loaded for every session. There is no per-project scoping beyond the directory config.
- Logs go to stderr. Stdout is not read by the host; stderr is passed through for debugging.
InputHandledstops all further processing. No agent turn is started, no message is appended to the transcript.BeforeCompactfires before the LLM call. Returnnilto let the default summarizer run. Return a*CompactionResultto supply your own summary — useful for using a cheaper model or domain-specific logic.