Getting started

TipJust want the shortest path for your OS?

Follow the focused platform guide, then come back here for the full model: Linux · Guix · macOS · Windows. Each covers the prerequisites, install, and — importantly — where your config file lives on that OS (macOS in particular puts it under ~/Library/Application Support, not ~/.config).

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 start

The 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_token
head -c 32 /dev/urandom | base64 > ~/.config/wraptool/auth_token

4. Connect your coding agent

wraptool init --harness claude --harness cursor --url http://127.0.0.1:8717/sse

This 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.

Important

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/sse

wraptool 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

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

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 up

Running bare wraptool in a repository does the same thing. up is idempotent and does three things:

  1. Ensures the shared server is running — if it isn’t, it starts wraptool serve detached (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 at 127.0.0.1:8718 with 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.
  2. 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 to devcontainer up (see below).
  3. 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 — when guix is on PATH and the worktree has a manifest.scm. It runs guix shell --container --network -m manifest.scm: no Docker, no image build, and the container gets the same toolchain you develop with (from manifest.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:

  • guix shares the host network namespace, so a loopback bind (127.0.0.1:8717) is reachable from inside the container. up leaves it as configured — no LAN exposure. The auth token is still auto-generated: a loopback bind is not a boundary against a browser, so sse requires a token either way.
  • devcontainer runs on a bridge network, where the container reaches the host via the bridge gateway, not the host’s loopback. So up rebinds a loopback mcp.listen to 0.0.0.0 (same port) and, since a non-loopback bind is reachable off-host, auto-generates an MCP auth token (defaulting mcp.auth_token_file if unset). The container’s harness config is wired to the detected gateway with that token. A direct wraptool serve on a non-loopback bind requires mcp.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 guix

require_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 allow

This 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.

Harness HOME is shared and writable

Every project receives the same writable harness HOME from $XDG_DATA_HOME/wraptool/harness-pool/home. This is also intentional: harness login, configuration, history, and skills are maintained once and persist across projects. Harness binaries and libraries are mounted read-only, but HOME state is read-write.

The consequence is that all projects using the pool form one harness-state trust domain. A compromised project can read or alter that state and leave persistent configuration for later projects. Keep unrelated credentials out of the pool, review shared skills/configuration before using high-risk repositories, and remove or recreate the pool when you need a clean trust boundary. Choose a separate XDG data directory or OS account when projects must not share harness state.

The shell opens automatically when you run up from a terminal. Control it with --shell:

wraptool up --shell=always   # always open a shell
wraptool up --shell=never    # just bring the container up (e.g. in scripts/CI)

One shared server can serve multiple projects and worktrees. The generated connection sends each host path as ?cwd=, but the client controls that value; configure mcp.allowed_cwd_roots when the server must enforce a path boundary.

Shut the project’s container down again with the counterpart command:

wraptool down          # stop this worktree's dev container
wraptool down --rm     # ...and remove it (next `up` recreates it fresh)

Manage the shared server (left running by down, since it serves other projects):

wraptool server status
wraptool server stop

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,java

That 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.

Important

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.

Back to top