Configuration
patchwire.yml
Lives at the root of each project on the laptop. Created by patchwire setup (preferred) or patchwire init.
project: my_flutter_appremote: host: <your-remote-host> user: <your-user> path: ~/workspace/my_flutter_app sshPort: 22 agentUrl: http://<your-remote-host>:7878 token: ${PW_TOKEN}sync: exclude: - build/ - .dart_tool/ - ios/Pods/ - node_modules/ - .git/ai: command: claude args: [--print] timeoutSec: 600Field reference
| Path | Type | Required | Default | Notes |
|---|---|---|---|---|
project | string | yes | (none) | Folder name on remote. Must match [a-zA-Z0-9_.-]+. |
remote.host | string | yes | (none) | Hostname or IP (Tailscale Magic-DNS recommended). |
remote.user | string | yes | (none) | SSH user on the remote. |
remote.path | string | yes | (none) | Absolute or ~-relative path on the remote. |
remote.sshPort | number | no | 22 | Override if SSH listens elsewhere. |
remote.agentUrl | URL | yes | (none) | Where the CLI will POST /ask. |
remote.token | string | yes | (none) | Bearer token. Use ${PW_TOKEN} interpolation. Don’t commit secrets. |
sync.exclude | string[] | no | [] | Paths to skip when syncing. Honored by both the CLI’s rsync (--exclude-from) and the extension’s two-way Mutagen sync (merged with a safety baseline). The setup wizard seeds this from a per-project-type sync profile (Flutter / Node frontend / Node backend / Python / Common), auto-detected and confirmable; edit it freely afterward. .git/ and .patchwire/ are always excluded. |
sync.secretScan | off|warn|block | no | off | Run a gitleaks scan over the files about to sync. warn reports findings and continues; block refuses the sync (override with --force). Closes the gap where a secret in a tracked file would otherwise cross. Best-effort: if gitleaks isn’t installed it logs and continues. |
ai.command | string | no | claude | Path or name of the AI CLI to spawn on the remote. |
ai.args | string[] | no | [--print] | Args passed to ai.command. The prompt is sent on stdin. |
ai.timeoutSec | number | no | 600 | Hard kill after this many seconds. |
Env var interpolation
Any ${VAR} in a string value is resolved from the laptop’s environment at config-load time. If the var is unset, the CLI fails fast with a clear error. Use this for secrets so they stay out of git.
remote: token: ${PW_TOKEN} # ✅ # token: hardcoded-abc # 🚫 don't do thisAgent environment variables
The agent reads its config exclusively from environment variables. patchwire-agent install writes them to ~/.patchwire/agent.env and embeds them in the launchd plist.
| Variable | Required | Default | Notes |
|---|---|---|---|
PW_AGENT_TOKEN | no | — | Legacy. Used only at first boot to auto-migrate to a per-user default user. After migration, manage users with patchwire-agent user add|list|rotate|disable|rm. |
PW_USERS_FILE | no | ~/.patchwire/users.json | Path to the agent’s users JSON. |
PW_PROJECTS_ROOT | yes | — | Parent directory containing each project. |
PW_AGENT_HOST | no | 127.0.0.1 | Bind interface. Use 0.0.0.0 to bind all, or your tailnet IP. |
PW_AGENT_PORT | no | 7878 | TCP port. |
PW_AI_BIN | no | claude | Path to the Claude CLI. |
PW_AI_ARGS | no | --print | Space-separated args. Add a JSON output format (e.g. --print --output-format json) to enable cost tracking (see below). |
PW_TIMEOUT_SEC | no | 600 | Hard kill timeout for claude. |
PW_VERIFY_CMD | no | — | Optional command run on the checkout after the diff is captured (e.g. flutter analyze, npm test). Its pass/fail is returned with /ask so you review a diff that already passed validation. Informs, never blocks. |
PW_VERIFY_TIMEOUT_SEC | no | 300 | Timeout for PW_VERIFY_CMD. |
PW_PRICING_FILE | no | ~/.patchwire/pricing.yml | Optional operator price table, used only to estimate cost when the provider doesn’t report one itself. |
PW_EGRESS | no | off | deny runs claude under a macOS seatbelt sandbox that blocks all outbound except localhost, DNS, and the allowlist. Fail-closed. See Default-deny egress. |
PW_EGRESS_ALLOW | no | — | Extra hosts (comma/space separated) the sandbox may reach, in addition to the Anthropic API. |
PW_EGRESS_ALLOW_DNS | no | 1 | 0 blocks DNS too (tightest; only works if claude reaches pinned IPs). |
PW_MAX_CONCURRENT_TOTAL | no | 3 | Maximum simultaneous Claude runs across all users. Requests beyond this cap wait FIFO. |
PW_MAX_CONCURRENT_PER_USER | no | 1 | Maximum simultaneous Claude runs from any one user. Prevents single-user hogging when the global cap allows it. |
PW_AUDIT_LOG | no | ~/.patchwire/agent.log | JSONL audit log path. One line per successful /ask or /chat turn. No plaintext prompts (only sha256). |
PW_AUDIT_LOG_MAX_BYTES | no | 52428800 (50 MiB) | Size threshold that triggers rotation to .1. |
PW_AUDIT_LOG_MAX_FILES | no | 3 | How many rotated tail files to keep (.1, .2, .3). Older files are dropped. |
The users, concurrency, and audit settings above work together to run one agent across a team. See Multi-developer for how they fit.
Cost tracking
patchwire-agent usage shows TOK (tokens) and $EQV (dollar cost) columns
once the agent can see real usage. It’s opt-in so the default stays unchanged:
-
Capture usage. Configure a JSON output format so the provider reports tokens (and, for Claude, cost) instead of plain text:
Terminal window PW_AI_ARGS="--print --output-format json"The agent parses the usage out and still shows you clean assistant text, so you never see raw JSON. With the default
--print, no usage is captured and the columns read—. -
Where the dollar figure comes from. We prefer the provider’s own reported cost (Claude’s
total_cost_usd, Aider’sCost:line), so there’s no price list to keep in sync. For a provider that reports tokens but not dollars, add an optional~/.patchwire/pricing.yml(orPW_PRICING_FILE); those rows show as~$…(estimated):models:"claude-opus-4-8": { in_per_mtok: 15.00, out_per_mtok: 75.00, cache_read_per_mtok: 1.50 }"gpt-5.2": { in_per_mtok: 10.00, out_per_mtok: 30.00 } -
What
$EQVmeans. Patchwire’s premise is one shared subscription. If that’s a flat-rate plan,$EQVis the API-equivalent cost: the right number for fair-share attribution across the team, not a second bill you pay.
Default-deny egress (macOS)
Stops the remote claude from sending data anywhere except the model API. It’s the
exfiltration counterpart to syncing only what you share. Opt-in, off by default.
# on the agent (e.g. in ~/.patchwire/agent.env)PW_EGRESS=deny# optional: also allow a package registry the agent fetches from mid-taskPW_EGRESS_ALLOW="registry.npmjs.org pub.dev"With deny, claude runs under a seatbelt (sandbox-exec) profile that blocks
all outbound except localhost, DNS, and the resolved allowlist (Anthropic API by
default). It’s fail-closed: if sandbox-exec is missing, the agent refuses
to start. Verify enforcement on the box before relying on it:
patchwire-agent egress-check# allowlisted (api.anthropic.com) reachable: YES ✅# non-allowlisted (example.com) blocked: YES ✅See Security → Default-deny egress.
Attachments
To let the remote claude read a local file (a screenshot, a PDF, any file at all,
including images for vision), get it onto the remote first. Both surfaces stage
the file into a gitignored .patchwire-inbox/ in the project (so it never lands
in a returned diff):
- CLI (for an SSH
claudesession):patchwire push ~/Desktop/shot.pngprints the remote path to paste;patchwire push --clippushes the current clipboard screenshot;patchwire push --cleanclears the inbox. - VS Code: the 📎 Attach file button (and the Attach clipboard image
command) stages the file, waits for sync, and types the remote path straight
into your active
claudesession terminal. The panel’s Attachments list shows everything currently staged, where you can open any file (images preview in VS Code) or delete one. Deleting clears it locally and, through two-way sync, from the remote.
Laptop environment variables
| Variable | Required | Notes |
|---|---|---|
PW_TOKEN | yes | Bearer token. Loaded by source ~/.patchwire/env after setup. |
PW_USER | yes (v0.2+) | The username the agent recognizes you as. Set by patchwire setup (defaults to os.userInfo().username). Used both for /me identity and in the rsync target path. |
PW_VERBOSE | no | Set to 1 to print debug info from the CLI. |
File layout
~/.patchwire/ # laptop or remote env # `export PW_TOKEN=...` (chmod 600) agent.env # remote only: `export PW_AGENT_TOKEN=...` (chmod 600) logs/agent.{out,err}.log # remote only: launchd stdout/stderr
<your-project>/ patchwire.yml # checked into git .patchwire/ # gitignored last.patch # most recent diff (optional)Updating config
Already know the IP? Three ways to set it directly
If you already have a working address for the remote (a fixed Tailscale IP, a LAN IP with a DHCP reservation, or a hostname), you don’t need to re-run the Tailscale picker.
1. Edit patchwire.yml by hand. It’s just YAML. Change remote.host and remote.agentUrl, save, run patchwire doctor to verify. No daemon to restart.
2. Re-run setup with --host (and --force to overwrite):
patchwire setup --force --host <your-ip>This skips Tailscale detection entirely. Other prompts still come up with their previous defaults. Answer or accept.
3. Fully non-interactive (all values from flags):
patchwire setup --force \ --host <your-ip> \ --user <your-user> \ --project my_app \ --path '~/workspace/${project}'See setup reference for every flag.
Other changes
patchwire.yml is just YAML. Edit it with any editor. Changes are picked up on the next ask. There’s no daemon or cache to restart.
After changing the agent’s env vars (for example, bumping PW_TIMEOUT_SEC):
launchctl unload ~/Library/LaunchAgents/com.patchwire.agent.plistlaunchctl load ~/Library/LaunchAgents/com.patchwire.agent.plistOr simpler: re-run patchwire-agent install with the new flags.