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, 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 agent’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 nothing up front. If no config exists at the resolved path it writes a commented starter there first, logs where it landed, and carries on — it never overwrites a config you already have. That starter is deny-by-default and allows exactly one thing, git pull, so the very first run comes up working and already demonstrates a credentialed action the AI cannot perform itself.
Editing that policy to expose the tools you actually want is the next step, not a prerequisite: see Create a config.
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, and read §2 when you are ready to widen the policy.
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 two privileged operations — git pull and git fetch — so the server comes up immediately; you then edit the file to expose the tools you want. push ships commented out and clone is deliberately absent; the scaffold explains why at each.
Only wrap commands that actually need wraptool. git pull, git fetch, and git push reach the network with your host credentials — which the agent 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 agent 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.
On sse, an auth token is mandatory, not optional. Leave auth_token_file unset and wraptool server start generates one at ~/.config/wraptool/mcp-token (mode 0600) and prints where it went. On unix, whose filesystem socket is already restricted to mode 0600, a token stays optional.
To use your own token instead, set the path and write the token yourself:
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 agent
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. On sse, init provisions the default token file itself if none exists yet, so the header is written whenever a wraptool configuration exists (run init after the first serve or up, which scaffold one — the order this guide follows). On unix, with no auth_token_file configured, 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 agent. The wraptool log prints one line per executed command with status and duration.
6. One command per project: wraptool up
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 review UI at127.0.0.1:8718with its own generated token (so you can approve the agent’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 agent 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. The auth token is still auto-generated: a loopback bind is not a boundary against a browser, sosserequires a token either way.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/harness/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 review 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. It is wiring only (major :1): it writes the MCP client config that points the container at the host wraptool and pins HOME/PATH, but it installs no harness binary — that comes from the shared harness pool’s runtime/ tree, built once per developer with wraptool harness install <name> on the host and mounted read-only into every project container. It layers onto any base image. See Harness pool: quarantine and scheduled refresh for keeping that pool’s candidate metadata current with a periodic wraptool harness refresh.
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 both shared-pool mounts (home/ read-write, runtime/ read-only) — all prefilled. 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": {},
// Wiring only — no binary installed here.
"forge.snamellit.com/pti/wraptool/wraptool:1": { "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 pool. runtime/ MUST be the string form ending in
// ",readonly": string form is CSV-parsed by the devcontainer CLI (so an
// interior space, e.g. macOS's own ~/Library/Application Support, survives
// intact) and it is the ONLY form the CLI honors a read-only flag on —
// object-form mounts have no read-only field at all in its schema and
// silently come up read-write regardless of any "readonly" key. home/ is
// read-write either way, so it stays object form purely for readability.
"mounts": [
{ "source": "${localEnv:HOME}/.local/share/wraptool/harness-pool/home", "target": "/home/wraptool-harness", "type": "bind" },
"source=${localEnv:HOME}/.local/share/wraptool/harness-pool/runtime,target=/opt/wraptool-harness/runtime,type=bind,readonly"
]
}
init devcontainer writes both mount sources 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,runtime}, on Windows ${localEnv:HOME}/AppData/Local/wraptool/harness-pool/{home,runtime}. ${localEnv:HOME} works on all three OSes — %LOCALAPPDATA% is itself %USERPROFILE%\AppData\Local, just a different relative path than the Linux/macOS defaults. The Windows-specific step is setting HOME at all (only USERPROFILE is set by default there) — see Windows. This matches where wraptool up creates the pool, so the bind never targets a missing directory. Install the harness once, on the host, before opening the container: wraptool harness install claude.
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, including the deprecated :0 per-image-provisioning major, which remains 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.