* fix(projects): restore cross-host grouping Random host-local project IDs replaced the remote-derived value that had also served as grouping identity. Persist a separate opaque group key and let normal boot reconciliation backfill older records without a migration. * fix(projects): keep ambiguous clones separate A shared remote can identify a project across hosts, but it cannot distinguish two independent clones on one host. Treat those keys as ambiguous so host-local project IDs remain separate. * fix(projects): preserve host-local project identity * fix(projects): strengthen grouping identity * test(app): update project host shape contract * fix(projects): preserve selected host identity * fix(projects): close grouped routing gaps * fix(projects): stabilize legacy subproject grouping * fix(projects): preserve selected project identity * fix(projects): isolate legacy host identity * fix(projects): preserve symlinked bootstrap roots * fix(projects): reconcile grouped custom names * fix(projects): stabilize settings and path identity * fix(projects): normalize default remote ports * fix(projects): harden host and remote identity * fix(projects): preserve host-scoped actions * fix(projects): preserve remote path semantics * fix(projects): model SCP remote semantics * fix(projects): keep settings on routed host * fix(projects): preserve SSH URL path semantics * fix(projects): separate identity from presentation * test(projects): use grouped identity in UI specs * fix(projects): harden grouped project routing * test(projects): expect framed settings route * fix(projects): preserve exact placement identity * fix(projects): harden grouping identity and routing * fix(projects): restore project key as grouping identity Random host-local project IDs had replaced the persisted cross-host value carried by projectKey. Keep projectId for routing while reconciliation owns and persists projectKey for grouping. * fix(projects): prevent project key collisions Preserve platform path semantics and generic remote transports in cross-host keys, and compare legacy checkout roots using filesystem equivalence before materializing ownership. * test(projects): assert host-scoped project keys * fix(projects): preserve project key compatibility Resolve legacy workspace project references at the daemon boundary while keeping projectKey as the cross-host identity. Complete generic SSH normalization without collapsing distinct repositories. * fix(projects): harden project reference identity * fix(projects): retain archived reference errors * fix(projects): scope legacy paths to hosts * test(app): model persisted project keys * fix(projects): retain absolute scp users * fix(projects): resolve legacy mutation aliases * fix(projects): preserve conservative identity boundaries * fix(projects): preserve legacy placement identity * test(projects): assert legacy placement identity * test(projects): expect canonical Windows keys * fix(projects): separate bootstrap identity fields * fix(projects): scope legacy paths by host * fix(projects): stabilize opaque project identity * fix(projects): preserve remaining opaque identities * fix(projects): classify forward-slash UNC roots * fix(projects): preserve opaque selection keys * fix(projects): frame host-local project keys * fix(projects): canonicalize framed identities * test(projects): isolate repeated remote fixtures * fix(projects): keep project key as UI identity * fix(projects): reconcile hydrated project references * fix(projects): preserve archived project identity * refactor(projects): group projects from project records, not workspaces Cross-host grouping had been built by hanging a projectKey off every workspace descriptor. That scoped the key to the wrong entity: the app then had to reconstruct project identity per workspace in six modules, which forced projectKey fallbacks into RPC arguments and a server-side resolver that turned grouping keys back into project ids. A workspace has a projectId. The key belongs to the project, and grouping is a display concern, so the daemon now publishes projects over project.list plus the existing project.update channel and the app groups them in one place. Projects are addressed by host-local projectId everywhere, including the settings route. Projects persisted without a key are backfilled by ordinary boot reconciliation, with no migration. Old daemons advertise no projectList capability and simply do not group. * test(projects): seed projects alongside workspaces in fixtures Sidebar fixtures seeded workspaces without their projects, which the daemon cannot produce now that projects arrive on their own channel, so nothing rendered. Adds a seeding helper that supplies both. Also drops the provisioning test for accepting a project key where a project id belongs; that resolver was removed deliberately. * fix(projects): keep derived project identity stable Four gaps left by shrinking the key derivation: Local keys used a bare path resolve, so one directory reached by a different spelling produced a different key and reset grouped identity. Windows CI caught this with the 8.3 and long-name forms of one temp path. Canonicalize through the shared realpath-aware normalization. Subproject display names lost their selected-path input, so every subproject of a monorepo rendered under the same remote-derived name. Reconciliation only ever backfilled a missing key, so a project whose remote changed stayed grouped with hosts on the old remote. Project settings kept a disconnected host selected and rendered a dead end with no way back to an editable host. * fix(projects): read scheme remotes as URLs, not scp form An `ssh://user@host:22/org/repo` remote matched the scp-form pattern first, because `[^@]+` happily eats `ssh://git`, so the port landed in the path and the project keyed as `remote:host/22/org/repo`. Two spellings of one remote therefore never grouped. Try scp form only when there is no scheme, and drop protocol-default ports so an explicitly written `:22` matches the same remote without it.
Paseo
One interface for Claude Code, Codex, Copilot, OpenCode, and Pi agents.
Run agents in parallel on your own machines. Ship from your phone or your desk.
- Self-hosted: Agents run on your machine with your full dev environment. Use your tools, your configs, and your skills.
- Multi-provider: Claude Code, Codex, Copilot, OpenCode, and Pi through the same interface. Pick the right model for each job.
- Voice control: Dictate tasks or talk through problems in voice mode. Hands-free when you need it.
- Cross-device: iOS, Android, desktop, web, and CLI. Start work at your desk, check in from your phone, script it from the terminal.
- Privacy-first: Paseo doesn't have any telemetry, tracking, or forced log-ins.
Getting Started
Paseo runs a local server called the daemon that manages your coding agents. Clients like the desktop app, mobile app, web app, and CLI connect to it.
Prerequisites
You need at least one agent CLI installed and configured with your credentials:
Desktop app (recommended)
Download it from paseo.sh/download or the GitHub releases page. Open the app and the daemon starts automatically. Nothing else to install.
To connect from your phone, open Settings → your host → Connections → Pair a device.
CLI / headless
Install the CLI and start Paseo:
npm install -g @getpaseo/cli
paseo
This shows a QR code in the terminal. Connect from any client. This path is useful for servers and remote machines.
For full setup and configuration, see:
Docker
Run the Paseo daemon and self-hosted web UI in Docker:
docker run -d --name paseo \
-p 6767:6767 \
-e PASEO_PASSWORD=change-me \
-v "$PWD/paseo-home:/home/paseo" \
-v "$PWD:/workspace" \
ghcr.io/getpaseo/paseo:latest
Open http://localhost:6767 after it starts. Extend the base image with the agent CLIs you use, then provide credentials through environment variables or the persistent /home/paseo volume. See the Docker documentation for full setup details.
CLI
Everything you can do in the app, you can do from the terminal.
paseo run --provider claude/opus-4.6 "implement user authentication"
paseo run --provider codex/gpt-5.4 --worktree feature-x "implement feature X"
paseo ls # list running agents
paseo attach abc123 # stream live output
paseo send abc123 "also add tests" # follow-up task
# run on a remote daemon
paseo --host workstation.local:6767 run "run the full test suite"
See the full CLI reference for more.
Skills
Skills teach your agent to use Paseo to orchestrate other agents.
npx skills add getpaseo/paseo
Then use them in any agent conversation:
/paseo-handoff— hand off work between agents. I use this to plan with Claude and then handoff to Codex to implement./paseo-loop— loop an agent against clear acceptance criteria (aka Ralph loops), optionally with a verifier./paseo-advisor— spin up a single agent as an advisor for a second opinion, without delegating the work itself./paseo-committee— form a committee of two contrasting agents to step back, do root cause analysis, and produce a plan.
Development
Quick monorepo package map:
packages/server: Paseo daemon (agent process orchestration, WebSocket API, MCP server)packages/app: Expo client (iOS, Android, web)packages/cli:paseoCLI for daemon and agent workflowspackages/desktop: Electron desktop apppackages/relay: Relay transport and encryption used by the daemon and clientspackages/website: Marketing site and documentation (paseo.sh)
Common commands:
# run all local dev services
npm run dev
# run individual surfaces
npm run dev:server
npm run dev:app
npm run dev:desktop
npm run dev:website
# build the server stack
npm run build:server
# repo-wide checks
npm run typecheck
Related projects
- getpaseo/paseo-relay — official distributed relay, written in Elixir
- paseo-skins — community themes and a zero-patch desktop theme loader with an Agent Skill
- paseo-vscode — VS Code extension
License
AGPL-3.0

