sequenceDiagram participant AI as AI assistant participant W as wraptool participant G as git AI->>W: tools/call git_pull W->>W: policy.Evaluate W->>G: exec /usr/bin/git pull (with host credentials) G-->>W: stdout W->>W: scrub credentials W-->>AI: ToolResult
Getting started
The recommended path: wraptool up
Once wraptool is installed and you have a config, you don’t wire anything by hand. In any git repo:
wraptool upThat one command starts the shared server (auto-generating the auth token), auto-provisions your coding assistant’s MCP config with the connection URL and Authorization header, brings up an isolated container (no credentials), and drops you into a shell. Bare wraptool in a repo does the same. Full detail in §6 below.
up needs one thing up front: an existing config. It calls config.Load before it does anything else, so with no config file it fails immediately — create one first (see Create a config). After that, up is all you run.
The sections below are the manual / from-scratch path — how the pieces fit when you want to build, configure, and connect a harness yourself. If you just ran up, skip to §6 for its details; you only need the config section below.
1. Build (manual path)
go build -o wraptool .On Guix systems, the bundled manifest gives you a reproducible toolchain:
guix time-machine -C channels.scm -- shell -m manifest.scm -- go build -o wraptool .2. Create a config
wraptool reads YAML from ~/.config/wraptool/config.yaml (XDG, with --config / WRAPTOOL_CONFIG overrides).
You usually don’t write this from scratch: on first run both wraptool up and wraptool serve scaffold a commented starter config at that path if none exists (never overwriting one that does) and log where it landed. The starter enables a single privileged tool — git pull — so the server comes up immediately; you then edit the file to expose the tools you want.
Only wrap commands that actually need wraptool. git pull, git fetch, and git push reach the network with your host credentials — which the AI never sees — so they must go through wraptool. Read-only local git (status, log, diff) only touches the working .git, needs no credentials, and the assistant can run it itself, so there is nothing to gain by wrapping it. A fuller policy:
mcp:
transport: sse
listen: 127.0.0.1:8717
tools:
git:
binary: /usr/bin/git
timeout: 30s
allow:
- subcommand: [pull]
flags: ["--ff-only", "--rebase"]
- subcommand: [fetch]
flags: ["--prune", "--all", "--depth"]
flag_constraints:
"--depth": { type: integer, max: 100 }
- subcommand: [push]
flags: ["--set-upstream"]
deny_flags: ["--force", "--force-with-lease"]
deny:
- subcommand: [remote, set-url]This policy wraps a CLI binary. Wraptool can also spawn credentialed local stdio MCP servers and expose selected tool names; see Proxy local MCP servers. Both capability sources appear on the same client-facing MCP connection.
See Configuration reference for every field.
3. Start the server
wraptool server startThe available transports:
| Transport | Config | Use case |
|---|---|---|
sse |
transport: sse, listen: host:port |
Independent process, TCP |
unix |
transport: unix, listen: /path/to.sock |
Independent process, Unix socket |
The TCP listener (sse/unix) serves both MCP HTTP transports on one port: legacy SSE at /sse (Claude Code, Cursor) and Streamable HTTP at /mcp (the 2025-03 transport — Antigravity and other clients that POST initialize to the URL). One Bearer-auth wrapper guards both; you don’t choose — each harness’s generated config points at the endpoint it speaks.
For SSE and Unix transports, add an auth token:
mcp:
transport: sse
listen: 127.0.0.1:8717
auth_token_file: ~/.config/wraptool/auth_tokenhead -c 32 /dev/urandom | base64 > ~/.config/wraptool/auth_token4. Connect your coding assistant
wraptool init --harness claude --harness cursor --url http://127.0.0.1:8717/sseThis writes the MCP config block for each harness. Supported values: agy, claude, cursor, opencode, gemini, pi, roo, or all.
init reads auth_token_file from your config and embeds the Authorization: Bearer header in every networked harness config — so an auth-enabled server works out of the box, no hand-editing. With no auth_token_file (localhost-trust) no header is written. agy (Antigravity) speaks Streamable HTTP, so its serverUrl targets /mcp rather than /sse; all others are written to the endpoint they use.
A harness running inside a container (e.g. Antigravity in its own container) cannot reach the server at 127.0.0.1 — that’s the container’s own loopback. Use the host’s docker-gateway address instead (typically 172.17.0.1), and bind the server where the container can reach it (listen: 0.0.0.0:8717 or the gateway), not 127.0.0.1:
wraptool init --harness agy --url http://172.17.0.1:8717/ssewraptool up’s devcontainer feature auto-detects this gateway for you; the manual init above is only needed when you wire a container-side harness yourself.
5. Smoke test
Trigger any allowed subcommand from your assistant. The wraptool log prints one line per executed command with status and duration.
6. One command per project: wraptool up
Once the config exists you don’t start things by hand. In any git repository (or worktree) root:
wraptool upRunning bare wraptool in a repository does the same thing. up is idempotent and does three things:
- Ensures the shared server is running — if it isn’t, it starts
wraptool servedetached (pidfile + lock under$XDG_RUNTIME_DIR, auto-generating the auth token), and waits for it to become reachable. It also brings up the operator review UI at127.0.0.1:8718with its own generated token (so you can approve the AI’s capability requests) and prints its URL — disable with--no-admin. The review UI is host-facing and always stays on loopback. - Brings up the project’s container, injecting the connection info: the auth token and this worktree’s host path. On a Guix host it uses a native Guix container built from your project’s
manifest.scm; elsewhere it falls back todevcontainer up(see below). - Drops you into a shell in the container so you can start developing immediately. Exiting the shell leaves the container running; re-enter with
wraptool up.
Which container runtime?
up auto-selects (and prints) the backend:
guix— whenguixis onPATHand the worktree has amanifest.scm. It runsguix shell --container --network -m manifest.scm: no Docker, no image build, and the container gets the same toolchain you develop with (frommanifest.scm), so the AI harness can compile, test, and analyze locally. The harness itself installs once into the shared pool ($XDG_DATA_HOME/wraptool/harness-pool/home, mounted to/home/wraptool-harness) — its login and skills persist there across every project, so you don’t re-authenticate per box.devcontainer— the fallback for non-Guix hosts (Arch, macOS, …); see Dev Containers.
Force one with --runtime=guix|devcontainer|auto.
The runtime also determines how the server is bound, so the container can actually reach it — you don’t configure this per platform:
guixshares the host network namespace, so a loopback bind (127.0.0.1:8717) is reachable from inside the container.upleaves it as configured — no LAN exposure.devcontainerruns on a bridge network, where the container reaches the host via the bridge gateway, not the host’s loopback. Souprebinds a loopbackmcp.listento0.0.0.0(same port) and, since a non-loopback bind is reachable off-host, auto-generates an MCP auth token (defaultingmcp.auth_token_fileif unset). The container’s harness config is wired to the detected gateway with that token. A directwraptool serveon a non-loopback bind requiresmcp.auth_token_file— it will not start unauthenticated.
Host CLI credentials never enter the container either way — the isolation is the mount/user namespace (your normal $HOME, SSH keys, kubeconfig, and cloud tokens are not mounted). git is available inside for local work (status/diff/commit/branch), but anything needing credentials (push, private fetch) goes through wraptool’s policed MCP git tool on the host. The Guix runtime intentionally shares the harness pool and host network as described below. See design/guix-container-runtime.md.
Guix: convenience versus isolation
The Guix defaults deliberately retain three conveniences that weaken isolation. They avoid a separate environment definition, repeated harness setup, and a Docker Compose stack for every local service. Understand these boundaries before running untrusted projects.
Approve executable Guix inputs with direnv
manifest.scm and channels.scm are executable Scheme, not inert lock files. Guix evaluates them on the host while constructing the environment. A container can modify the writable worktree, including manifest.scm, so use direnv’s required-file approval before use guix:
require_allowed manifest.scm
if [[ -f channels.scm ]]; then
require_allowed channels.scm
fi
use guixrequire_allowed watches the files and records their content as part of direnv allow. If either changes, direnv refuses to run use guix again until you review the change and explicitly run:
direnv allowThis adds no prompt when the files are unchanged and one normal direnv approval after an intentional edit. It protects the direnv workflow only: direct guix shell -m manifest.scm, automation, and the current implementation of wraptool up do not consult direnv’s approval database. Use a direnv release that provides require_allowed; do not replace it with an unconditional custom use_guix wrapper.
Host localhost is reachable
The Guix runtime uses --network, which shares the host network namespace. Processes in the container can therefore connect to services bound only to host loopback, including local PostgreSQL, Redis, MongoDB, and test HTTP servers. This is intentional: those services can be reused directly without constructing a per-project Docker Compose environment.
Loopback is consequently a reachability boundary, not an authorization boundary. Assume untrusted project code can probe and use every host localhost service:
- Require authentication where the service supports it.
- Do not keep production data or privileged unauthenticated admin endpoints on ports reachable during a session.
- Use separate test databases and least-privilege test accounts.
- Keep wraptool’s admin UI authenticated; its token is intentionally not passed to the container.
Wiring the container: the wraptool feature
The container side is handled by the single wraptool dev container feature, published to the Forgejo OCI registry. Keyed on one harness choice, it does both jobs via toggles that default to true: provision installs the harness binary into the image, and connect writes the MCP client config that points it at the host wraptool. It layers onto any base image.
Rather than hand-write the .devcontainer/devcontainer.json, scaffold it:
wraptool init devcontainer --harness claude --lang go,javaThat writes the committed config from the base:ubuntu + language-features model: the languages you list, the wraptool feature with your harness, the WRAPTOOL_* env, and the shared-pool mount — all prefilled. npm harnesses (claude/opencode/pi) auto-pull the node feature so the build won’t fail for lack of npm. It writes a fresh file and refuses to clobber an existing one (pass --force); edit the result freely. The emitted config looks like:
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/go:1": {},
"ghcr.io/devcontainers/features/node:1": {}, // for the npm harness
"forge.snamellit.com/pti/wraptool/wraptool:0": { "harness": "claude" }
},
// Host workspace path (so the server scopes tools to this worktree).
"containerEnv": { "WRAPTOOL_CWD": "${localWorkspaceFolder}" },
// Token is a secret → remoteEnv keeps it out of image layers.
"remoteEnv": { "WRAPTOOL_TOKEN": "${localEnv:WRAPTOOL_TOKEN}" },
// Share the host harness-state pool (login/skills once, every project).
// Object form so a source path with a space (macOS ~/Library/Application
// Support) is never mis-split by the mount-string parser.
"mounts": [
{ "source": "${localEnv:HOME}/.local/share/wraptool/harness-pool/home", "target": "/home/wraptool-harness", "type": "bind" }
]
}
init devcontainer writes the mount source for your host OS — the .local/share above is the Linux form; on macOS it is ${localEnv:HOME}/Library/Application Support/wraptool/harness-pool/home, on Windows ${localEnv:HOME}/AppData/Local/wraptool/harness-pool/home (forward slashes on every OS). It matches where wraptool up creates the pool, so the bind never targets a missing directory.
At container create the feature writes the MCP client config pointing at the host wraptool, auto-detecting the host as the container’s default-route gateway — so it works across docker networks and worktrees with no host.docker.internal or --add-host. See Dev Containers for every option. The older wraptool-connect feature (wiring only) is deprecated but stays published for existing configs.
For container use the server must listen where the container can reach it — bind the docker gateway or 0.0.0.0 (e.g. listen: 0.0.0.0:8717), not 127.0.0.1. See Isolated environments.