OpenCode + OmO
Local secrets, model routing, corporate overlays, contextual notifications, and local voice dictation.
On this page15 sections
chezmoi, the dotfile manager, manages the shared OpenCode configuration, Oh My OpenAgent routing, fish integration, and tmux notification plumbing. Secrets and company-specific endpoints stay local.
What is managed
~/.config/opencode/opencode.json
Plugins, formatters, language servers, and personal MCP configuration.
~/.omo/omo.jsonc
Agent categories, model choices, fallbacks, and disabled hooks.
~/.config/fish/conf.d/opencode.fish
Activates the optional corporate configuration.
~/.config/tmux/tmux.conf
Installs the contextual-notifier companion plugin.
~/.config/opencode/tui.json
Keybinds, the managed tokyonight-transparent theme, and the client-side plugins including voice dictation.
~/.config/opencode/AGENTS.md
Instructions applied to every session, including the mise and gh rules.
opencode.json declares the opencode-claude-auth, Oh My OpenAgent, and
contextual-notifier plugins. The TUI loads its own tui.json, which declares
Oh My OpenAgent again alongside the voice plugin. OpenCode installs every
declared plugin with Bun when it starts.
First-run checklist
- Apply the dotfiles with the setup guide.
- Create the local secret files if you want to use the Home Assistant MCP server.
- Put only the secret value in each file—no quotes or variable names.
- Start a new fish shell so an optional corporate overlay is detected.
- Restart all running OpenCode processes after changing plugin declarations.
Local secrets
The managed OpenCode file references two files that are intentionally not tracked by Git:
~/.config/opencode/ secrets/ home-assistant-mcp-url ~/.config/opencode/ secrets/ home-assistant-access-token
Create them with restrictive permissions:
install -d -m 700 "$HOME/.config/opencode/secrets"install -m 600 /dev/null "$HOME/.config/opencode/secrets/home-assistant-mcp-url"install -m 600 /dev/null "$HOME/.config/opencode/secrets/home-assistant-access-token"Edit each file and store only its value. OpenCode resolves the {file:...}
references when it loads the configuration; chezmoi never reads or copies the
secret contents.
The home-assistant server ships with "enabled": false, so populating the
files does not switch it on by itself. Enable it in the managed configuration or
in a corporate overlay once the values are in place.
Corporate overlay
Put company-specific OpenCode configuration in:
~/.config/opencode/opencode.corp.jsonThis file is unmanaged and should use mode 0600:
install -m 600 /dev/null "$HOME/.config/opencode/opencode.corp.json"When the file exists, the managed opencode fish wrapper function sets
OPENCODE_CONFIG to its path for that invocation—but only when the current
directory is under ~/ghq/workgit/. Personal repositories always use the
default configuration, even on a machine that has the corporate overlay. Keep
company endpoints, profiles, and credentials in the overlay rather than adding
them to the personal repository. The overlay can use the same {file:...}
syntax for credentials stored in separate local files.
The routing is decided per invocation from the working directory, so no shell restart is needed after creating or removing the overlay.
Model routing
~/.omo/omo.jsonc
is the routing source of truth. It assigns primary and fallback models to named
agents and task categories, using a fallback when the first choice is
unavailable.
Model names change more often than the surrounding workflow, so consult the
managed file for the current assignments rather than copying a list from this
page. The session-notification hook is disabled there because notifications
are handled by the dedicated contextual-notifier plugin.
Contextual notifications
OpenCode declares the
opencode-contextual-notifier
package in the managed
opencode.json.
Its tmux companion is declared through TPM in the managed
tmux.conf.
Together they mark the originating tmux window when an OpenCode session needs attention and clear that state when the window is selected or focused. The notifier package repository contains its implementation and tests; this dotfiles repository only declares and configures it.
After changing the notifier declaration:
- Restart OpenCode so Bun can synchronize the package.
- Reload tmux with Ctrl+A then Ctrl+R.
- Run the TPM installation flow if the companion plugin is not present.
Voice dictation
Ctrl+R records a prompt, transcribes it, and inserts the cleaned text into the prompt box. Both models run on this machine, so no audio leaves it.
The plugin is declared in the managed
tui.json,
which also frees
Ctrl+R by disabling
the factory session_rename binding—rename a session with /rename instead.
The plugin talks to two local services:
127.0.0.1:8081
whisper.cpp behind a managed LaunchAgent, biased with a project vocabulary.
127.0.0.1:11434
ollama serving voice-normalize, which repunctuates the raw transcript.
The models are roughly 3 GB, so they are installed on demand rather than during provisioning:
mise run voice:setupThe task is idempotent. It verifies the whisper checksum, brings ollama up, rebuilds the derived model from its Modelfile, restarts the transcription service, and probes both endpoints before reporting success.
| Managed file | Role |
|---|---|
bin/whisper-voice-server | Starts whisper-server with the vocabulary as its initial prompt. |
bin/ollama-serve | Starts ollama serve and caps its log. |
.config/ollama/voice-normalize.Modelfile | Pins the base checkpoint and bakes in deterministic sampling. |
.config/opencode/voice-vocabulary.txt | Tool names fed to whisper so it stops mangling them. |
.config/opencode/voice-stt-prompt.md | System prompt that cleans up the raw transcript. |
Both services are LaunchAgents declared in this repository rather than
brew services entries, so the bind address, tuning flags, and log paths are
reviewable here instead of being whatever the Homebrew formula ships. Nothing
in this repository starts brew services, so a machine that had the Homebrew
ollama service running needs it stopped once by hand—it binds the same port.
| Managed service | Role |
|---|---|
Library/LaunchAgents/com.shmileee.whisper-voice-server.plist | Keeps whisper.cpp running and owns ~/Library/Logs/whisper-voice-server.log. |
Library/LaunchAgents/com.shmileee.ollama.plist | Keeps ollama serve bound to loopback and owns ~/Library/Logs/ollama.log. |
Neither log is rotated by launchd, so each wrapper truncates its own in place once it grows past a limit.
Editing the vocabulary or the prompt
The two text files reload differently:
voice-vocabulary.txtis read bywhisper-serverwhen it starts, so it needs a service restart:chezmoi applythenmise run voice:setup.voice-stt-prompt.mdis read by the plugin when OpenCode starts, so it needschezmoi applyand an OpenCode restart.voice:setupdoes nothing for it.
Deliberately unmanaged
opencode.corp.jsonCompany-specific configurationopencode/secrets/Credentials and private endpointsvoice:setupTroubleshooting
An MCP server fails to start
Confirm that both secret files exist, contain a value, and use mode 0600:
stat -f '%Sp %N' "$HOME/.config/opencode/secrets/"* # macOSstat -c '%A %n' "$HOME/.config/opencode/secrets/"* # LinuxThe corporate configuration is ignored
OPENCODE_CONFIG is set per invocation by the opencode wrapper function,
only inside ~/ghq/workgit/—a bare echo $OPENCODE_CONFIG in a shell is
expected to print nothing. Confirm the overlay file exists and that the
wrapper resolves it from a corporate checkout:
cd ~/ghq/workgit/Trackunit/<repo>test -f ~/.config/opencode/opencode.corp.json; and echo overlay presentfunctions opencode | grep -q workgit; and echo wrapper activeA notification or tmux marker is stale
Focus the originating tmux window first. If the marker remains, restart OpenCode and reload tmux configuration with Ctrl+A then Ctrl+R.
Voice recording does nothing
Check that the transcription service is loaded, running, and answering:
launchctl print "gui/$(id -u)/com.shmileee.whisper-voice-server" | grep 'state ='curl -s http://127.0.0.1:8081/healthA job that is loaded but not running means the wrapper diagnosed something and stopped deliberately—it reports success so launchd does not retry a condition only a person can clear. The reason is at the end of its log:
tail -n 20 ~/Library/Logs/whisper-voice-server.logIt reports two: a missing model, cleared by mise run voice:setup, and another
process already holding the port.
lsof -nP -iTCP:8081 -sTCP:LISTENDictation is transcribed but the inserted text is wrong
That is the normalization step rather than whisper. Confirm its agent is up and the derived model answers:
launchctl print "gui/$(id -u)/com.shmileee.ollama" | grep 'state ='ollama list | grep voice-normalizecurl -s http://127.0.0.1:11434/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{"model":"voice-normalize","max_tokens":16,"messages":[{"role":"user","content":"ping"}]}'mise run voice:setup runs the same probe and rebuilds the model from its
Modelfile.
If the agent will not stay up, check that a Homebrew service is not competing for the port. Only a machine provisioned before this agent existed can have one, and stopping it is a one-time fix:
brew services list | grep ollamabrew services stop ollama