~/oleksandr-ponomarov

Portfolio · Notes · Dotfiles

Search everything

Search case studies, engineering notes, and Dotfiles documentation.

    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

    OmO ~/.omo/omo.jsonc

    Agent categories, model choices, fallbacks, and disabled hooks.

    OpenCode TUI ~/.config/opencode/tui.json

    Keybinds, the managed tokyonight-transparent theme, and the client-side plugins including voice dictation.

    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

    1. Apply the dotfiles with the setup guide.
    2. Create the local secret files if you want to use the Home Assistant MCP server.
    3. Put only the secret value in each file—no quotes or variable names.
    4. Start a new fish shell so an optional corporate overlay is detected.
    5. 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:

    Create them with restrictive permissions:

    Terminal window
    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.json

    This file is unmanaged and should use mode 0600:

    Terminal window
    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:

    1. Restart OpenCode so Bun can synchronize the package.
    2. Reload tmux with Ctrl+A then Ctrl+R.
    3. 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:

    Transcription 127.0.0.1:8081

    whisper.cpp behind a managed LaunchAgent, biased with a project vocabulary.

    Normalization 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:

    Terminal window
    mise run voice:setup

    The 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 fileRole
    bin/whisper-voice-serverStarts whisper-server with the vocabulary as its initial prompt.
    bin/ollama-serveStarts ollama serve and caps its log.
    .config/ollama/voice-normalize.ModelfilePins the base checkpoint and bakes in deterministic sampling.
    .config/opencode/voice-vocabulary.txtTool names fed to whisper so it stops mangling them.
    .config/opencode/voice-stt-prompt.mdSystem 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 serviceRole
    Library/LaunchAgents/com.shmileee.whisper-voice-server.plistKeeps whisper.cpp running and owns ~/Library/Logs/whisper-voice-server.log.
    Library/LaunchAgents/com.shmileee.ollama.plistKeeps 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:

    Deliberately unmanaged

    opencode.corp.jsonCompany-specific configuration
    opencode/secrets/Credentials and private endpoints
    Runtime filesCaches, backups, lockfiles, and dependency directories
    Voice modelsThe whisper weights and ollama blobs installed by voice:setup

    Troubleshooting

    An MCP server fails to start

    Confirm that both secret files exist, contain a value, and use mode 0600:

    Terminal window
    stat -f '%Sp %N' "$HOME/.config/opencode/secrets/"* # macOS
    stat -c '%A %n' "$HOME/.config/opencode/secrets/"* # Linux

    The 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:

    Terminal window
    cd ~/ghq/workgit/Trackunit/<repo>
    test -f ~/.config/opencode/opencode.corp.json; and echo overlay present
    functions opencode | grep -q workgit; and echo wrapper active

    A 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:

    Terminal window
    launchctl print "gui/$(id -u)/com.shmileee.whisper-voice-server" | grep 'state ='
    curl -s http://127.0.0.1:8081/health

    A 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:

    Terminal window
    tail -n 20 ~/Library/Logs/whisper-voice-server.log

    It reports two: a missing model, cleared by mise run voice:setup, and another process already holding the port.

    Terminal window
    lsof -nP -iTCP:8081 -sTCP:LISTEN

    Dictation 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:

    Terminal window
    launchctl print "gui/$(id -u)/com.shmileee.ollama" | grep 'state ='
    ollama list | grep voice-normalize
    curl -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:

    Terminal window
    brew services list | grep ollama
    Terminal window
    brew services stop ollama