paseo-profiles · git:20260911.53e2570 · 2026-09-11 · sha256 8bd34d7ec4d216f7

paseo-profiles git:20260911.53e2570A

Immutable. This exact content is served forever at /api/v1/blob/8bd34d7ec4d216f7.

---
name: paseo-profiles
description: Copy paseo agent profiles (Architect, Engineer, Reviewer, Writer…) from one machine to another by merging them into the target's ~/.paseo/config.json, over ssh, idempotently. Use when the user wants to export/import/sync/clone paseo agent profiles between hosts, set up paseo agent profiles on a new or freshly installed machine, or roll the same profile set across a fleet. paseo itself has no profile export/import command — `paseo agent` manages running agents, not profile templates.
---

# paseo-profiles

Move **agent profiles** — the templates that pair a name, provider, model, mode, and thinking level
(“Software Engineer” = claude / claude-opus-5 / bypassPermissions / xhigh) — between machines.

paseo has no command for this. The profiles are a JSON array at `daemon.agentProfiles` in
`~/.paseo/config.json`, authored by hand in a paseo client UI (desktop, mobile, or the daemon's
own web UI) and reachable from no CLI.

## Why not just scp config.json

That same file holds **host-specific** settings: `daemon.listen`, `daemon.relay.enabled`,
`daemon.cors`, and any password hash written by `paseo daemon set-password`. Copying the file
wholesale flips those on the target. `~/.paseo/server-id`, `cli-client-id` and `daemon-keypair.json`
are per-host identity and must never be copied either. So this is a **key-scoped merge**, not a file copy.

## Usage

One command does the whole round trip:

```bash
~/.claude/skills/paseo-profiles/scripts/paseo-profiles.py push dev-server-frank-lume
```

Multiple hosts are just more arguments; one host failing doesn't abort the rest.

```bash
paseo-profiles.py push host-a host-b host-c --dry-run   # report only, write nothing
```

Always offer `--dry-run` first when the target already has profiles of its own.

The halves are also usable separately, for an air-gapped or reviewed transfer:

```bash
paseo-profiles.py export -o profiles.json     # on the source
paseo-profiles.py import profiles.json        # on the target (also reads stdin)
```

Other flags: `--config PATH` (default `~/.paseo/config.json`), `--no-reload`.
`paseo-profiles.py selftest` checks the merge logic.

## What it carries

| Key path | How |
|---|---|
| `daemon.agentProfiles` | merged element-wise |
| `daemon.mcp.injectIntoAgents` | scalar overwrite |
| `daemon.browserTools.enabled` | scalar overwrite |
| `agents.providers.omp.enabled` | scalar overwrite |

Every other key in the target is read, left alone, and written back verbatim.

## Merge rules

Per incoming profile: **id match** → replace in place; else **name match** under a different id →
replace that slot, incoming id wins (importing “Software Engineer” must not leave two of them); else
**append**. Profiles only the target has are never deleted. Re-running is a no-op — it prints
`no changes` and skips the write and reload entirely.

## Reference

[`references/config-and-merge.md`](references/config-and-merge.md) covers the profile fields on
disk, the export envelope and its versioning, how each incoming profile resolves, the atomic write,
and how the reload report is read. Reach for it when a merge did something unexpected, when adding
a key to the synced set, when changing the envelope version, or when a reload reports a path back.

## Safety

Backs up to `config.json.bak-<UTC>` before the first write, writes via a temp file at mode `0600`
then `os.replace` (atomic; the file holds secrets on some hosts). A missing target config is created
rather than treated as an error.

## Gotchas

- **`paseo` is not on the non-interactive ssh PATH on every host.** `ssh host 'paseo …'` can fail
  with *not found* while `ssh host 'zsh -lc paseo'` works. Use an absolute path —
  `~/.local/share/mise/shims/paseo` on hosts running the npm CLI, `/opt/homebrew/bin/paseo` on any
  still on Paseo.app. The script resolves this itself, in that order.
- **A stopped daemon is fine.** Reload is best-effort; a stopped daemon reads the config at next
  start, and the script says so instead of failing.
- After writing, it reads `paseo daemon reload --json` and reports honestly — including when the
  edit will be ignored because a launch-time flag outranks the file. See the reference.
- Profiles land regardless of whether the target has that provider installed. A Codex profile on a
  host without Codex shows as unavailable — a gap on that host, not a transfer failure.
- `push` stages the script at `/tmp/paseo-profiles-$(id -u).py` on the remote and runs it with
  `python3`, so the target needs python3 on its ssh PATH (macOS with CLT or mise: yes).