Gary is an open-source reference implementation of a generic, read-only Slack investigator backed by a long-running OpenCode web server. It defaults to cloning and inspecting this repository, making the example useful without access to private systems.
Slack --Socket Mode--> Go dispatcher --HTTP--> OpenCode server + web UI
|-- shell/tools
`-- /workspace/repos/gary
Each Slack thread maps to one OpenCode session. Session titles encode the Slack channel and thread timestamp, so the dispatcher recovers mappings after restart. Turns within a thread run one at a time, and duplicate Socket Mode deliveries (reconnects, overlapping event subscriptions) are dropped. Replies use Slack's native Markdown block, turns have a 30-minute timeout, and an optional public OpenCode URL lets Gary post a session link before work begins.
Kubernetes runs two containers in one pod from one image: OpenCode owns durable sessions and the self-checkout; the small Go bot connects over pod-local localhost. Docker Compose uses two services because containers do not share localhost, so the bot uses http://opencode:4096. Both topologies persist OpenCode data/config and repository checkouts.
Gary is instructed to investigate, not mutate. OpenCode's edit permission is denied and the shipped GitHub/Polylane skills are read-only. This is not a complete sandbox: the model still has shell access, which is powerful. Use isolated infrastructure and narrowly scoped, preferably read-only credentials.
main.go,internal/slackbot: health wait, Socket Mode dispatcher, thread/session recovery, and Slack renderinginternal/opencode: focused OpenCode HTTP client and opt-in live smoke testopencode.json,AGENTS.md,bundled-skills: model configuration, persona, and investigation guidanceDockerfile,entrypoint.sh,docker-compose.yml: reproducible image and local two-service topologykustomize/gary: one-pod/two-container base with dev and prod examplesTiltfile,Taskfile.yml,.github/workflows: development and CI
- Create a Slack app and enable Socket Mode.
- Add bot scopes
app_mentions:read,chat:write,channels:history,groups:history,im:history, andreactions:write. - Subscribe to
app_mention,message.channels,message.groups, andmessage.im. - Create an app-level token with
connections:write, install the app, and invite it to desired channels. - Put the bot token (
xoxb-...) and app token (xapp-...) in.env.
Gary responds to DMs, mentions, and follow-ups in threads it already owns (including replies sent with "also send to channel"). It does not listen indiscriminately to every channel message. Mentions are handled from either the app_mention or message event, so overlapping subscriptions are safe and mention-only setups still work.
Copy .env.example to .env. Set OPENCODE_MODEL to an OpenCode model identifier such as openai/gpt-5, then supply the provider's native environment variable (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.). opencode.json uses official {env:OPENCODE_MODEL} substitution and does not define a custom provider.
Optional settings include OPENCODE_PUBLIC_URL, a scoped GH_TOKEN for private forks or GitHub queries, and Polylane credentials. The public self-checkout does not require a GitHub token.
cp .env.example .env
# replace the dummy Slack/provider values
docker compose up --buildThe opencode service runs the entrypoint and opencode web; the bot service bypasses that entrypoint and runs gary-bot. The image build requires no secret.
Prerequisites: Go 1.26, Task, kustomize, and optionally yamlfmt, Docker, and Tilt.
go mod tidy
task do # format, build/vet, test, manifest rendering
task container:validate # optional Docker buildUnit tests are hermetic — they run the real client and dispatcher against in-process fake Slack and OpenCode servers, with the race detector on. The OpenCode smoke test is skipped unless OPENCODE_URL points to a live server. For Tilt, create an uncommitted .env; the Tiltfile generates gary-secrets locally and applies the dev overlay.
The runtime ships Bash/core Unix tools, git, ripgrep, jq, curl, Python/uv, GitHub CLI, and Polylane CLI. Bundled skills cover the single local self-checkout, generic read-only GitHub inspection, and generic read-only Polylane investigation. Override GARY_GITHUB_REPOSITORY, branch, destination/root, or refresh interval to inspect a fork.
The kustomize base uses one replica, Recreate, a PVC, a ClusterIP Service for the web UI, and an optional gary-secrets Secret. The pod does not mount a serviceaccount token and containers drop all capabilities, since the model has shell access inside them. Set the image and provide Slack/provider environment values through your secret manager. Keep one replica because Socket Mode ownership and in-memory thread mappings are not distributed.
CI checks formatting, build, vet, tests, and both overlays. The container workflow builds pull requests without pushing; pushes to main and v* tags publish to ghcr.io/${owner}/gary using GitHub's built-in token.
- Read-only behavior depends on instructions, permissions, credential scope, and runtime isolation; shell access remains a security boundary.
- Session recovery depends on OpenCode's session title/list APIs and the pinned OpenCode version.
- Gary has no private SaaS, database, cluster, or company-specific integrations in this reference.
- The default checkout is shallow and periodically refreshed from one configured branch.
See CONTRIBUTING.md, SECURITY.md, and the MIT license.