Skip to content

Configuration

patchwire.yml

Lives at the root of each project on the laptop. Created by patchwire setup (preferred) or patchwire init.

project: my_flutter_app
remote:
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: 600

Field reference

PathTypeRequiredDefaultNotes
projectstringyes(none)Folder name on remote. Must match [a-zA-Z0-9_.-]+.
remote.hoststringyes(none)Hostname or IP (Tailscale Magic-DNS recommended).
remote.userstringyes(none)SSH user on the remote.
remote.pathstringyes(none)Absolute or ~-relative path on the remote.
remote.sshPortnumberno22Override if SSH listens elsewhere.
remote.agentUrlURLyes(none)Where the CLI will POST /ask.
remote.tokenstringyes(none)Bearer token. Use ${PW_TOKEN} interpolation. Don’t commit secrets.
sync.excludestring[]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.secretScanoff|warn|blocknooffRun 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.commandstringnoclaudePath or name of the AI CLI to spawn on the remote.
ai.argsstring[]no[--print]Args passed to ai.command. The prompt is sent on stdin.
ai.timeoutSecnumberno600Hard 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 this

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

VariableRequiredDefaultNotes
PW_AGENT_TOKENnoLegacy. 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_FILEno~/.patchwire/users.jsonPath to the agent’s users JSON.
PW_PROJECTS_ROOTyesParent directory containing each project.
PW_AGENT_HOSTno127.0.0.1Bind interface. Use 0.0.0.0 to bind all, or your tailnet IP.
PW_AGENT_PORTno7878TCP port.
PW_AI_BINnoclaudePath to the Claude CLI.
PW_AI_ARGSno--printSpace-separated args. Add a JSON output format (e.g. --print --output-format json) to enable cost tracking (see below).
PW_TIMEOUT_SECno600Hard kill timeout for claude.
PW_VERIFY_CMDnoOptional 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_SECno300Timeout for PW_VERIFY_CMD.
PW_PRICING_FILEno~/.patchwire/pricing.ymlOptional operator price table, used only to estimate cost when the provider doesn’t report one itself.
PW_EGRESSnooffdeny 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_ALLOWnoExtra hosts (comma/space separated) the sandbox may reach, in addition to the Anthropic API.
PW_EGRESS_ALLOW_DNSno10 blocks DNS too (tightest; only works if claude reaches pinned IPs).
PW_MAX_CONCURRENT_TOTALno3Maximum simultaneous Claude runs across all users. Requests beyond this cap wait FIFO.
PW_MAX_CONCURRENT_PER_USERno1Maximum simultaneous Claude runs from any one user. Prevents single-user hogging when the global cap allows it.
PW_AUDIT_LOGno~/.patchwire/agent.logJSONL audit log path. One line per successful /ask or /chat turn. No plaintext prompts (only sha256).
PW_AUDIT_LOG_MAX_BYTESno52428800 (50 MiB)Size threshold that triggers rotation to .1.
PW_AUDIT_LOG_MAX_FILESno3How 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:

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

  2. Where the dollar figure comes from. We prefer the provider’s own reported cost (Claude’s total_cost_usd, Aider’s Cost: 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 (or PW_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 }
  3. What $EQV means. Patchwire’s premise is one shared subscription. If that’s a flat-rate plan, $EQV is 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.

Terminal window
# on the agent (e.g. in ~/.patchwire/agent.env)
PW_EGRESS=deny
# optional: also allow a package registry the agent fetches from mid-task
PW_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:

Terminal window
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 claude session): patchwire push ~/Desktop/shot.png prints the remote path to paste; patchwire push --clip pushes the current clipboard screenshot; patchwire push --clean clears 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 claude session 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

VariableRequiredNotes
PW_TOKENyesBearer token. Loaded by source ~/.patchwire/env after setup.
PW_USERyes (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_VERBOSEnoSet 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):

Terminal window
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):

Terminal window
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):

Terminal window
launchctl unload ~/Library/LaunchAgents/com.patchwire.agent.plist
launchctl load ~/Library/LaunchAgents/com.patchwire.agent.plist

Or simpler: re-run patchwire-agent install with the new flags.