Integrating external tools¶
VibePod agents run in containers, so plugging in third-party tooling comes down to where the tool lives:
| Tool | What it does | Where it runs | Pattern |
|---|---|---|---|
| LiteLLM | Model gateway: one endpoint and key for 100+ providers | Its own container | Service on the VibePod network |
| Headroom | Compression proxy: shrinks tool output, logs and files before they reach the model | Its own container | Service on the VibePod network |
| RTK | Rewrites shell commands so the agent reads compact output | Inside the agent container | Tool in the agent image |
| Graphify | Knowledge graph of the codebase the agent can query instead of grepping | Inside the agent container | Tool in the agent image |
Both patterns use configuration VibePod already has — no plugin or code change is involved. This page describes each pattern once and then applies it to the four tools from issue #130.
Services on the VibePod network¶
Every vp run creates (or reuses) the Docker network named by the network:
config key — vibepod-network unless you changed it — and attaches the agent
and the built-in vibepod-proxy container to it. The
fastest way to make a service reachable from agents is to start it on that
network under a stable alias — the same wiring
vibepod-board
uses:
# compose.yml of the tool you want to integrate
services:
mytool:
image: example/mytool:latest
networks:
vibepod:
aliases:
- mytool # agents reach it as http://mytool:<port>
networks:
vibepod:
name: ${VIBEPOD_NETWORK:-vibepod-network}
external: true
external: truemakes compose join the network instead of creating its own. The network has to exist first: run any agent once (vp run claude) or create it by hand withdocker network create <network>. If you changed thenetwork:key in your VibePod config, tell the tool's compose file which one to join by settingVIBEPOD_NETWORKwhen you rundocker compose up— it only drives the interpolation above; VibePod itself reads thenetwork:config key.- No
ports:are needed. The service is reachable from containers on the network — agents and the proxy — but not from the host or the LAN. Publish a port only when you also want the tool's web UI in your browser. - The alias is plain container DNS. On Podman this needs a DNS-enabled network; see Quickstart — Using Podman.
Plain HTTP between containers
The examples on this page use http://<alias>:<port>, so API keys and
subscription tokens cross the container network unencrypted. A network
does not encrypt traffic between its containers. That is acceptable on a
single-host bridge network that only your own containers join; it is not
once the network spans hosts (an overlay or a remote engine) or runs
containers you do not trust. In those cases put TLS in front of the
service (a reverse proxy, or the tool's own TLS options) and use an
https:// URL.
Pointing the agent at the service¶
Agents are configured through environment variables, so referencing the
service means setting the right variable to http://<alias>:<port>. Pick
the scope you need:
# ~/.config/vibepod/config.yaml (global) or .vibepod/config.yaml (project)
agents:
claude:
env:
ANTHROPIC_BASE_URL: http://mytool:4000
For LLM gateways the llm: section sets base URL, key and model
for Claude Code in one place. It has no per-agent scope: once enabled it is
applied to every agent with an LLM mapping, and its Codex mapping passes only
the base URL and model (Codex's --oss mode, no key). When Codex routes
through an authenticated gateway with its own provider config, as shown
below, leave llm: off and configure Claude through agents.claude.env.
Some agents read endpoints from their own config files rather than the
environment (Codex's config.toml, Pi's models.json, Tau's
catalog.toml). Those files live in the agent's persisted config directory,
which is mounted into the container on every run:
$ vp config path
Config: /home/me/.config/vibepod
Global: /home/me/.config/vibepod/config.yaml
Project: /home/me/project/.vibepod/config.yaml
Logs: /home/me/.config/vibepod/logs.db
Proxy: /home/me/.config/vibepod/proxy/proxy.db
Config: is the root. Below it, agents/<agent>/ is the default
profile and profiles/<name>/agents/<agent>/ a named one; the
container sees that directory as its config or home mount, so a file you edit
there is what the agent reads. Pinning a profile per project (profile: work
in .vibepod/config.yaml) keeps a "via gateway" credential set next to a
direct one.
The built-in proxy¶
Agents send outbound HTTP through vibepod-proxy, and that includes requests
to a sidecar alias — calls to http://litellm:4000 show up in vp logs like
any other traffic. Two consequences:
- In
allowfilter mode, allow the alias:vp proxy filter allow add litellm. - To bypass the proxy for the sidecar (say, to compare latencies), extend
NO_PROXY:-e NO_PROXY=localhost,127.0.0.1,::1,litellm.
Tools with their own compose network¶
If a tool ships a compose file you would rather not edit, keep its network and connect the agent to it instead:
When the workspace itself contains a compose file, vp run offers this
interactively — see
Connecting to a Docker Compose network.
LiteLLM¶
LiteLLM is an OpenAI- and Anthropic-compatible
gateway: one endpoint, one key, any provider behind it, plus spend tracking,
virtual keys and fallbacks. It serves /v1/messages, so Claude Code talks to
it natively, and /v1/chat/completions and /v1/responses for OpenAI-style
clients.
LiteLLM as a VibePod model provider
LiteLLM can also be registered with vp provider add as a
model provider and selected per launch with
vp run <agent> --provider <name>. A provider has one protocol, so
register one entry per protocol you need: anthropic for Claude (base URL
without /v1), openai-responses for Codex, openai-chat for Pi, Qwen,
Tau, Jcode and OpenCode (base URL ending in /v1). Authenticated
provider launches require an https:// URL, so this route fits a LiteLLM
deployment behind TLS. The plain http://litellm:4000 sidecar below,
with its master key, is wired through the llm: section and agent env
instead. The host cannot resolve the litellm alias, so enter model IDs
by hand rather than using discovery.
1. Start it on the VibePod network
# litellm/compose.yml
services:
litellm:
image: docker.litellm.ai/berriai/litellm:latest
command: ["--config", "/app/config.yaml", "--port", "4000"]
volumes:
- ./config.yaml:/app/config.yaml:ro
environment:
LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY:?set a master key}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
networks:
vibepod:
aliases:
- litellm
networks:
vibepod:
name: ${VIBEPOD_NETWORK:-vibepod-network}
external: true
# litellm/config.yaml — model_name is what agents will ask for
model_list:
- model_name: claude-sonnet-4-6
litellm_params:
model: anthropic/claude-sonnet-4-6
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: gpt-5.3-codex
litellm_params:
model: openai/gpt-5.3-codex
api_key: os.environ/OPENAI_API_KEY
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
cd litellm
LITELLM_MASTER_KEY=sk-litellm-... \
ANTHROPIC_API_KEY=sk-ant-... \
OPENAI_API_KEY=sk-... \
docker compose up -d
Export a key for every provider in model_list; a missing one reaches
LiteLLM as an empty string and only that model's requests fail. Drop the
gpt-5.3-codex entry (and OPENAI_API_KEY) if you only route Claude Code.
2. Point agents at http://litellm:4000
Set the base URL, the key and the model name from model_list in
Claude's own env, so the setting stays scoped to Claude:
# .vibepod/config.yaml
agents:
claude:
env:
ANTHROPIC_BASE_URL: http://litellm:4000
ANTHROPIC_API_KEY: sk-litellm-... # master key or a LiteLLM virtual key
ANTHROPIC_AUTH_TOKEN: sk-litellm-...
ANTHROPIC_MODEL: claude-sonnet-4-6
ANTHROPIC_DEFAULT_OPUS_MODEL: claude-sonnet-4-6
ANTHROPIC_DEFAULT_SONNET_MODEL: claude-sonnet-4-6
ANTHROPIC_DEFAULT_HAIKU_MODEL: claude-sonnet-4-6
These are the variables the llm: section would set. llm: is
shorter, but it is not per agent: enabled, it also launches Codex as
codex --oss -m claude-sonnet-4-6 against http://litellm:4000, which
overrides the provider set up in the Codex tab. Use it only when Claude
Code is the one agent you route through LiteLLM.
Codex selects providers in its config.toml, which lives in the
persisted agent directory: <Config>/agents/codex/.codex/config.toml
(create it if missing; profiles/<name>/agents/codex/... for a named
profile).
model = "gpt-5.3-codex"
model_provider = "litellm"
[model_providers.litellm]
name = "litellm"
base_url = "http://litellm:4000/v1"
env_key = "LITELLM_API_KEY"
Then hand Codex the key named in env_key:
Any agent that accepts an OpenAI-compatible base URL works the same way,
e.g. Qwen Code with OPENAI_BASE_URL: http://litellm:4000/v1 in
agents.qwen.env. Agents with their own provider files are covered in
Agents: Pi's models.json, Tau's catalog.toml,
Jcode's config.toml.
LiteLLM's Claude Code and Codex guides cover the agent side in more depth.
Headroom¶
Headroom is a compression proxy between
the agent and the provider: it shrinks tool output, logs and file contents in
the prompt before they reach the model, and keeps the originals retrievable.
Its headroom wrap command is a host-side workflow (start a local proxy,
launch the agent); inside VibePod you run its plain proxy mode and let the
container network do the wiring.
1. Start it on the VibePod network
# headroom/compose.yml
services:
headroom:
# The image's entrypoint is `headroom proxy`, listening on 0.0.0.0:8787.
image: ghcr.io/headroomlabs-ai/headroom:latest
environment:
HOME: /home/nonroot
HEADROOM_WORKSPACE_DIR: /home/nonroot/.headroom
HEADROOM_CONFIG_DIR: /home/nonroot/.headroom/config
# HEADROOM_OUTPUT_SHAPER: "1" # also trim what the model writes back
volumes:
- headroom-state:/home/nonroot/.headroom # savings ledger, CCR cache, logs
networks:
vibepod:
aliases:
- headroom
volumes:
headroom-state:
networks:
vibepod:
name: ${VIBEPOD_NETWORK:-vibepod-network}
external: true
Headroom forwards the credentials the agent sends, so the proxy needs no
provider key of its own: Claude Code keeps using its login (API key or
subscription token). Leave port 8787 unpublished — on the VibePod network only
agents can reach it. If you publish it anyway, set HEADROOM_PROXY_TOKEN as
Headroom's own compose file requires.
2. Point agents at it
With HEADROOM_PROXY_TOKEN set, Headroom answers 401 to every caller that
is not on its own loopback — agents on the VibePod network included — unless
the request carries the token. Authorization: Bearer is already taken by the
provider credential, so send it in Headroom's dedicated header through
Claude Code's ANTHROPIC_CUSTOM_HEADERS:
# .vibepod/config.yaml
agents:
claude:
env:
ANTHROPIC_BASE_URL: http://headroom:8787
ANTHROPIC_CUSTOM_HEADERS: "X-Headroom-Proxy-Token: <HEADROOM_PROXY_TOKEN value>"
OpenAI-style clients use http://headroom:8787/v1; for Codex put that in a
model_providers block as shown for LiteLLM.
3. Watch the savings
Compression happens after the VibePod proxy has seen the request, so
vp logs reports the uncompressed sizes. Headroom's own numbers live in its
container:
To chain Headroom in front of LiteLLM, point Headroom's upstream at the
gateway (ANTHROPIC_TARGET_API_URL: http://litellm:4000 in its environment)
and the agents at Headroom.
Tools inside the agent container¶
RTK and Graphify are not services. They are programs the agent invokes, plus a hook or skill that tells the agent to use them. Two VibePod features cover that:
- A project overlay installs the binary into the agent
image once — content-addressed, cached, shared with the team through the
committed
.vibepod/overlay/directory. - The registration step writes into the agent's persisted config dir. For
Claude Code the container sets
CLAUDE_CONFIG_DIR=/claude, which is the host'sagents/claude/directory (seevp config path); both RTK and Graphify honor that variable, so a hook or skill registered once is there on every later run and in every project.
One overlay Dockerfile per agent
VibePod builds exactly one fragment per agent: .vibepod/overlay/<agent>/Dockerfile
if it exists, otherwise the shared .vibepod/overlay/Dockerfile. The two do
not stack — once claude/Dockerfile exists, the shared file is ignored for
Claude. The RTK example below uses the shared file and the Graphify example
the per-agent one; to install both, put both into the same file as shown in
RTK and Graphify together.
Run the registration inside a session so the files belong to your user:
Claude Code's bash mode executes a line starting with ! in the container as
the agent user. agents.<agent>.init can automate it, but the init wrapper
replaces the image entrypoint, so the user-switching step is bypassed: init
commands (and the agent that follows them) run as root, and on Linux the
files they create in the config dir end up root-owned.
RTK¶
RTK rewrites the shell commands an agent
runs (git status → rtk git status, pytest → rtk pytest, …) and returns
compact output — up to 90 % less bash output for the agent to read.
1. Install the binary via an overlay
# .vibepod/overlay/Dockerfile — no FROM line
ADD https://github.com/rtk-ai/rtk/releases/download/v0.48.0/rtk-x86_64-unknown-linux-musl.tar.gz /tmp/rtk-x86_64.tar.gz
ADD https://github.com/rtk-ai/rtk/releases/download/v0.48.0/rtk-aarch64-unknown-linux-gnu.tar.gz /tmp/rtk-aarch64.tar.gz
# SHA-256 values from the release's checksums.txt; update them with the version.
RUN arch="$(uname -m)" \
&& case "$arch" in \
x86_64) sum=e4e650fa1677c0de2f6839a6040d7b17f312d32f163c402b75af70e9e5af1a91 ;; \
aarch64) sum=5ed65486a96077bd6bba7c87fdc9d0e4a1918d19619be3c87380888389a30c7c ;; \
*) echo "no RTK build for $arch" >&2; exit 1 ;; \
esac \
&& echo "$sum /tmp/rtk-$arch.tar.gz" | sha256sum -c - \
&& tar -xzf "/tmp/rtk-$arch.tar.gz" -C /usr/local/bin rtk \
&& chmod 755 /usr/local/bin/rtk \
&& rm /tmp/rtk-*.tar.gz
- Architecture — the archive is picked by
uname -minside the build, i.e. by the image's platform, not the host's. That matters because some agents pinlinux/amd64and run emulated on an arm64 host. Overlays build with the classic builder, soTARGETARCHis not available;ADDcannot be conditional, hence both downloads. Note the different libc suffix of the two builds (muslvs.gnu). - Pinned and verified — with a moving URL the overlay cache would keep whatever it downloaded first, and the checksum check fails the build if the archive is not the one you reviewed. The hashes are written into the fragment rather than fetched from the same release, so a tampered release cannot vouch for itself.
- Some RTK filters shell out to ripgrep; the
vibepod/claudeimage ships it.
2. Register the hook once
In a running vp run claude session:
--auto-patch skips the consent prompt. RTK adds a PreToolUse hook to
/claude/settings.json and a short RTK.md next to it; restart the agent to
load the hook. RTK's other integrations (rtk init -g --codex, --gemini,
--opencode, --agent pi, --agent vibe) follow the same shape — the
supported agents table
lists the mechanism each one uses.
The hook covers Bash tool calls only; Claude Code's built-in Read, Grep
and Glob tools bypass it. RTK's savings statistics (rtk gain) live in the
container user's home, which is not persisted — the hook is.
Graphify¶
Graphify parses the codebase
into a knowledge graph (graphify-out/ in the workspace) that the agent
can query instead of grepping: /graphify . builds it; graphify query,
path and explain answer questions against it. The Python package is
graphifyy (double y).
1. Install the CLI via an overlay
# .vibepod/overlay/claude/Dockerfile — no FROM line
ARG GRAPHIFY_VERSION=0.9.73
RUN apt-get update && apt-get install -y --no-install-recommends python3-venv \
&& rm -rf /var/lib/apt/lists/* \
&& python3 -m venv /opt/graphify \
&& /opt/graphify/bin/pip install --no-cache-dir "graphifyy==${GRAPHIFY_VERSION}" \
&& ln -s /opt/graphify/bin/graphify /usr/local/bin/graphify
The version is pinned, but its dependencies (tree-sitter grammars, numpy,
networkx, …) still resolve at build time. To lock the whole set, compile a
hashed requirements file once from a one-line graphifyy==0.9.73 input —
e.g. uv pip compile --generate-hashes --python-version <X.Y> -o requirements.txt,
with <X.Y> the image's python3 --version — commit it as
.vibepod/overlay/claude/requirements.txt (the fragment's directory is its
build context) and install with --require-hashes:
# .vibepod/overlay/claude/Dockerfile — no FROM line
COPY requirements.txt /tmp/graphify-requirements.txt
RUN apt-get update && apt-get install -y --no-install-recommends python3-venv \
&& rm -rf /var/lib/apt/lists/* \
&& python3 -m venv /opt/graphify \
&& /opt/graphify/bin/pip install --no-cache-dir --require-hashes \
-r /tmp/graphify-requirements.txt \
&& ln -s /opt/graphify/bin/graphify /usr/local/bin/graphify
The vibepod/claude image ships python3 but no pip, hence the venv. A
fixed path under /opt keeps the interpreter location stable regardless of
the HOME the agent runs with — the skill records that path in
graphify-out/ and reuses it later.
2. Register the skill
Once per config dir, in a running vp run claude session:
This writes /claude/skills/graphify/SKILL.md and a pointer in
/claude/CLAUDE.md. To ship the skill with the project instead, run
graphify install --project in the workspace (from the session, or on the
host with pipx install graphifyy); it writes .claude/skills/graphify/ for
you to commit. For Codex use graphify install --platform codex, which
targets ~/.codex — the persisted agents/codex/ mount.
The default install only nudges the agent to run graphify query before
reading files; it may still grep. For Claude Code, graphify install --project
--strict blocks the first raw source read of a session and redirects it to
the graph (once per session, then back to the nudge); GRAPHIFY_HOOK_STRICT=1
or 0 in agents.claude.env toggles that at runtime.
Then, in the agent:
RTK and Graphify together¶
Because the per-agent fragment replaces the shared one, a Claude image with
both tools needs both installs in .vibepod/overlay/claude/Dockerfile, with
the Graphify requirements.txt next to it:
.vibepod/overlay/
├── Dockerfile # optional: RTK only, for every other agent
└── claude/
├── Dockerfile # RTK + Graphify for Claude
└── requirements.txt # hashed Graphify dependencies
# .vibepod/overlay/claude/Dockerfile — no FROM line
# RTK
ADD https://github.com/rtk-ai/rtk/releases/download/v0.48.0/rtk-x86_64-unknown-linux-musl.tar.gz /tmp/rtk-x86_64.tar.gz
ADD https://github.com/rtk-ai/rtk/releases/download/v0.48.0/rtk-aarch64-unknown-linux-gnu.tar.gz /tmp/rtk-aarch64.tar.gz
RUN arch="$(uname -m)" \
&& case "$arch" in \
x86_64) sum=e4e650fa1677c0de2f6839a6040d7b17f312d32f163c402b75af70e9e5af1a91 ;; \
aarch64) sum=5ed65486a96077bd6bba7c87fdc9d0e4a1918d19619be3c87380888389a30c7c ;; \
*) echo "no RTK build for $arch" >&2; exit 1 ;; \
esac \
&& echo "$sum /tmp/rtk-$arch.tar.gz" | sha256sum -c - \
&& tar -xzf "/tmp/rtk-$arch.tar.gz" -C /usr/local/bin rtk \
&& chmod 755 /usr/local/bin/rtk \
&& rm /tmp/rtk-*.tar.gz
# Graphify
COPY requirements.txt /tmp/graphify-requirements.txt
RUN apt-get update && apt-get install -y --no-install-recommends python3-venv \
&& rm -rf /var/lib/apt/lists/* \
&& python3 -m venv /opt/graphify \
&& /opt/graphify/bin/pip install --no-cache-dir --require-hashes \
-r /tmp/graphify-requirements.txt \
&& ln -s /opt/graphify/bin/graphify /usr/local/bin/graphify
Keep the shared .vibepod/overlay/Dockerfile only if other agents should get
RTK too; Claude no longer reads it, so the RTK lines live in both files.
Verifying the wiring¶
# VIBEPOD_NETWORK as for the compose files: your `network:` value (see `vp config show`)
docker network inspect "${VIBEPOD_NETWORK:-vibepod-network}" \
--format '{{range .Containers}}{{.Name}} {{end}}'
vp config show # merged config: env, llm and init sections
vp logs start # requests to a sidecar alias appear like any other traffic