{"id":"send-to-paseo","repo":"tomgrin10/send-to-paseo","url":"https://github.com/tomgrin10/send-to-paseo/tree/main/plugin","name":"send-to-paseo","description":"Local HTTP bridge that dispatches PR work to the right Paseo workspace and agent.","categories":["github","browser","productivity"],"platforms":[],"caveats":["Full browser-side functionality needs the companion Chrome extension from the same repo (not distributed via this registry)","Needs `git` on the daemon host; `gh` is optional but improves PR title/branch detection and Graphite stack ranking"],"images":["https://raw.githubusercontent.com/tomgrin10/send-to-paseo/main/docs/screenshots/hero-github-pr-popover.png"],"themes":[],"health":{"manifestValid":true,"hasReadme":true,"hasLicense":true,"hasTests":true,"hasTypecheckScript":true,"updatedRecently":true},"scannedAt":"2026-10-15T12:34:27.082Z","addedAt":"2026-09-10T15:56:08+03:00","path":"plugin","version":"1.4.2","security":{"status":"passed","blockingFindings":0,"advisoryFindings":0,"scannedAt":"2026-10-15T12:14:24.405Z","commit":"99dabffd27faac2b05c8c0bd1280947305df8e60"},"author":"Tom Gringauz","license":"MIT","paseoVersionRequirement":">=0.9.0","descriptionNodes":[{"type":"text","text":"Local HTTP bridge that dispatches PR work to the right Paseo workspace and agent."}],"caveatNodes":[[{"type":"text","text":"Full browser-side functionality needs the companion Chrome extension from the same repo (not distributed via this registry)"}],[{"type":"text","text":"Needs "},{"type":"text","text":"git","code":true},{"type":"text","text":" on the daemon host; "},{"type":"text","text":"gh","code":true},{"type":"text","text":" is optional but improves PR title/branch detection and Graphite stack ranking"}]],"manifest":{"id":"send-to-paseo","requirements":{"paseo":">=0.9.0"}},"repoMeta":{"stars":7,"defaultBranch":"main","pushedAt":"2026-10-13T13:24:10Z"},"owner":{"login":"tomgrin10","avatarUrl":"https://avatars.githubusercontent.com/u/17593920?v=4"},"installNotesHtml":"<p>Plugins are trusted, unsandboxed code: this one runs an HTTP server that can\nstart agents which execute arbitrary code on the daemon machine. Read the source\nbefore installing it.</p>\n<p>One command from npm. No clone or build step:</p>\n<pre><code class=\"language-sh\">paseo plugin install send-to-paseo\npaseo plugin ls          # expect: send-to-paseo  running  yes\npaseo plugin logs send-to-paseo\n</code></pre>\n<p><code>pluginsEnabled</code> must already be <code>true</code> in the daemon's <code>config.json</code>.</p>\n<p>There is nothing to install because the plugin imports nothing at runtime that\nthe Paseo host does not already provide — see\n<a href=\"#no-runtime-dependencies-ever\">No runtime dependencies, ever</a>. To update an\ninstalled plugin, run <code>paseo plugin update send-to-paseo</code>; for a checkout,\nrun <code>paseo plugin reload send-to-paseo</code>.</p>\n<h3>From a checkout (contributors)</h3>\n<p>Installing from a directory is the loop to use while editing, because\n<code>paseo plugin reload</code> picks up changes in place:</p>\n<pre><code class=\"language-sh\">git clone https://github.com/tomgrin10/send-to-paseo.git\ncd send-to-paseo/plugin\nnpm install              # devDependencies only — for `npm run typecheck`\nnpm run verify\npaseo plugin add /absolute/path/to/send-to-paseo/plugin\npaseo plugin ls          # expect: send-to-paseo  running  yes\n</code></pre>\n<p>After editing the source:</p>\n<pre><code class=\"language-sh\">npm run verify\npaseo plugin reload send-to-paseo\n</code></pre>\n<p>Never restart the daemon to pick up plugin changes — that kills running agents.</p>\n<h3>No runtime dependencies, ever</h3>\n<p><code>paseo plugin add</code> compiles the plugin <strong>with no packages installed</strong> — there is\n…</p>","readmeText":"# send-to-paseo\n\n[![npm version](https://img.shields.io/npm/v/send-to-paseo?style=for-the-badge&color=cb3837)](https://www.npmjs.com/package/send-to-paseo)\n[![npm downloads](https://img.shields.io/npm/dm/send-to-paseo?style=for-the-badge&color=cb3837)](https://www.npmjs.com/package/send-to-paseo)\n[![Paseo](https://img.shields.io/badge/Paseo-%E2%89%A5%200.9.0-8A63D2?style=for-the-badge)](https://paseo.sh)\n[![License](https://img.shields.io/github/license/tomgrin10/send-to-paseo?style=for-the-badge&color=2563eb)](LICENSE)\n\nA Paseo plugin that runs a small local HTTP bridge so a browser extension can\nsend work from a pull request to the right workspace and agent — creating a\nworktree checked out to the PR if none exists.\n\n![The Send to Paseo composer open on a live github.com pull request — rails/rails #58627, state Open, merging Shopify:actionpack-singleton-class-attrs into rails:main — with the button anchored in GitHub's own PR header action row beside Code, and the popover below it showing the resolved target workspace, the target picker, the typed instruction \"Fix the flaky test in this PR\", the Provider and Mode selects, and the Cmd-Enter / Esc footer with Send enabled](https://raw.githubusercontent.com/tomgrin10/send-to-paseo/main/docs/screenshots/hero-github-pr-popover.png)\n\nThe extension never talks to the Paseo daemon. It talks only to this bridge,\nwhich speaks the frozen API in [the repository contract](https://github.com/tomgrin10/send-to-paseo/blob/main/CONTRACT.md) and reaches\nPaseo through the supported SDK.\n\n- Bridge: `http://127.0.0.1:7788`, loopback only, bearer token required.\n- Surface: **Send to Paseo** in the Paseo sidebar — status, token, agent destination,\n  new-agent defaults, recent sends.\n- Verified behaviour, with real command output, is recorded in the\n  [repository](https://github.com/tomgrin10/send-to-paseo/blob/main/plugin/VERIFICATION.md).\n\n---\n\n## Install\n\nPlugins are trusted, unsandboxed code: this one runs an HTTP server that can\nstart agents which execute arbitrary code on the daemon machine. Read the source\nbefore installing it.\n\nOne command from npm. No clone or build step:\n\n```sh\npaseo plugin install send-to-paseo\npaseo plugin ls          # expect: send-to-paseo  running  yes\npaseo plugin logs send-to-paseo\n```\n\n`pluginsEnabled` must already be `true` in the daemon's `config.json`.\n\nThere is nothing to install because the plugin imports nothing at runtime that\nthe Paseo host does not already provide — see\n[No runtime dependencies, ever](#no-runtime-dependencies-ever). To update an\ninstalled plugin, run `paseo plugin update send-to-paseo`; for a checkout,\nrun `paseo plugin reload send-to-paseo`.\n\n### From a checkout (contributors)\n\nInstalling from a directory is the loop to use while editing, because\n`paseo plugin reload` picks up changes in place:\n\n```sh\ngit clone https://github.com/tomgrin10/send-to-paseo.git\ncd send-to-paseo/plugin\nnpm install              # devDependencies only — for `npm run typecheck`\nnpm run verify\npaseo plugin add /absolute/path/to/send-to-paseo/plugin\npaseo plugin ls          # expect: send-to-paseo  running  yes\n```\n\nAfter editing the source:\n\n```sh\nnpm run verify\npaseo plugin reload send-to-paseo\n```\n\nNever restart the daemon to pick up plugin changes — that kills running agents.\n\n### No runtime dependencies, ever\n\n`paseo plugin add` compiles the plugin **with no packages installed** — there is\nno `npm install` step in that path, and the daemon's bundler can only resolve the\nspecifiers the host provides at runtime:\n\n```\n@getpaseo/plugin   @getpaseo/plugin/client   @getpaseo/plugin/server\n@getpaseo/plugin/client/react-native\nzod   react   react/jsx-runtime   react-native   @tanstack/react-query\nnode:*  (built-ins, in the server bundle)\n```\n\nEverything in `plugin/package.json` is therefore a **devDependency**, present only\nso `npm run typecheck` works for contributors.\n\n> **Nothing may be added to `dependencies`.** A runtime import of anything outside\n> the list above fails `paseo plugin add` for every user with\n> `Build failed: Could not resolve \"<pkg>\"`, while still working perfectly on any\n> machine that has run `npm install` — so it will not be caught locally. Imports\n> from `@getpaseo/client` and `@getpaseo/protocol` must stay `import type` (erased\n> at build time) or be reimplemented locally; the one runtime use of the Paseo SDK\n> goes through the assembled-specifier `require` in `server/daemon.ts`, precisely\n> so the bundler cannot see it. Before changing an import, read\n> [`VERIFICATION.md`](VERIFICATION.md) §18, which includes the exact\n> no-`node_modules` build command that proves an install still works.\n\n### Pairing\n\n1. Open **Send to Paseo** in the Paseo sidebar.\n2. Under **Pairing token**, press **Copy** (or **Reveal** and copy by hand).\n3. Paste it into the extension's options page. Simple setup already uses the Primary Paseo\n   machine's loopback bridge URL.\n\n**Regenerate** issues a new token and immediately invalidates the old one, so any\npaired extension has to be re-paired.\n\n---\n\n## Requirements\n\nTwo external commands, one required and one optional. **Nothing here is fatal\nexcept `git`** — with no `gh` at all you can still open the popover, pick a\nworkspace and start an agent, because Paseo checks the pull request out itself\nthrough its own forge credentials. `gh` only supplies metadata.\n\n| | Required? | Version | What it is used for | What breaks without it |\n| --- | --- | --- | --- | --- |\n| **Paseo** | yes | Latest release | Everything. The surface uses configured-host discovery and host-targeted client APIs. | The plugin does not load. |\n| **`git`** | yes | `2.51.2` | Reading the branch a workspace is on, and the repository's `origin`. Paseo itself needs it to create a worktree. | Creating a worktree fails with a message naming `git`. Workspace branches read as unknown, so nothing is ranked as an exact or stack match — everything falls back to \"create\". |\n| **`gh`** | **no** | `2.98.0` | PR title, head and base branch names, and stack discovery (`gh pr list` rebuilds the whole Graphite stack, including its merged and closed members). | Sending still works. You lose the PR title, the branch names, exact/stack candidate ranking, and the `Title:`/`Branch:` lines in the agent's prompt. Stack detection is lost **entirely**, local git ancestry included: that check proves \"this branch is an ancestor of a branch in the stack\", and without `gh` there is no stack and no PR head branch to compare against. The bridge says so in the target picker, in the agent's prompt and in the log. |\n\nNode is not a separate requirement: the plugin runs inside the Paseo daemon's\nown Node runtime.\n\n### Install commands\n\n```sh\n# macOS\nxcode-select --install       # git\nbrew install gh              # optional\n\n# Debian / Ubuntu\nsudo apt install git\nsudo apt install gh          # optional; see cli.github.com for other distros\n\n# Fedora / RHEL\nsudo dnf install git\nsudo dnf install gh          # optional\n```\n\nThen, if you installed `gh`:\n\n```sh\ngh auth login                # optional, but this is what makes titles and stacks work\ngh auth status               # should print the account and no error\n```\n\n### Where the plugin looks for them\n\nThis matters more than it sounds. A Paseo plugin runs in a daemon subprocess,\nand the daemon is normally started by the desktop app rather than by your shell.\n`/Applications/Paseo.app` itself is launched with\n\n```\nPATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin\n```\n\n— no `/opt/homebrew/bin`. Paseo enriched `PATH` for plugin subprocesses in the\noriginal `0.7.0` measurement, but the plugin does\nnot rely on that. Every lookup searches, in order:\n\n1. `SEND_TO_PASEO_GH_PATH` / `SEND_TO_PASEO_GIT_PATH`, if set;\n2. every directory on the subprocess's `PATH`;\n3. a short list of well-known install locations — `/opt/homebrew/bin`,\n   `/usr/local/bin`, `/usr/bin`, `/bin`, `/opt/local/bin`, `~/.local/bin`,\n   `/home/linuxbrew/.linuxbrew/bin`, `/snap/bin`, `/usr/local/git/bin`.\n\nSet `SEND_TO_PASEO_BIN_DIRS` to replace step 3 with your own colon-separated\nlist if your tools live somewhere else entirely.\n\nNothing is ever spawned through a shell. `gh` is a shell *function* in some\npeople's `zsh`, so the plugin resolves the executable itself and calls it with an\nargv array.\n\n### Dependency self-check\n\nEvery plugin start logs one line per command, plus the `PATH` it actually got:\n\n```\n[send-to-paseo] dependency git: ok — git version 2.51.2 at /opt/homebrew/bin/git\n[send-to-paseo] dependency gh: ok — gh version 2.98.0 (2026-08-20) at /opt/homebrew/bin/gh\n[send-to-paseo] plugin subprocess PATH=~/.local/bin:...:/opt/homebrew/bin:...\n```\n\n```sh\npaseo plugin logs send-to-paseo\n```\n\nThe same information is on the **Requirements** card in the Paseo surface, which\nis the place to look first if you have never opened a log file.\n\n---\n\n## Configuration\n\nAll of it lives in the Paseo surface; there is no config file to hunt for.\n\n| Setting | Default | Notes |\n| --- | --- | --- |\n| Port | `7788` | Saving rebinds the listener straight away. Bind address is always `127.0.0.1`. |\n| Private bridge address (`externalBridgeUrl`) | none | Optional HTTPS origin for a reverse proxy such as Tailscale Serve. Saving explicitly allowlists only that origin's Host header; the listener remains loopback-only. |\n| Paseo host discovery | automatic | Paseo supplies every configured host and live status, including offline hosts. “Check Paseo access” always calls `getPaseoClient(serverId)` for that exact row. |\n| Additional browser routes | none | Connections imported from `stp1_…` codes. These remain necessary only because host discovery exposes neither a private bridge URL nor its token; the Primary plugin resolves and sends through these bridges. |\n| Pairing token | generated on first run | 32 random bytes, base64url. |\n| Agent destination | `New agent` | Start a fresh agent, or reuse the best non-archived root agent in an existing target workspace. A create target and a workspace with no eligible root still start a new agent. |\n| Default model | the daemon's own default | `provider/model`, e.g. `claude/claude-opus-5`. A send may override it per request. |\n| Agent profile | none | One of your saved Paseo profiles (`daemon.agentProfiles`), followed by id. |\n| Default permission mode | follow Paseo | A mode id, e.g. `auto`. Mode ids are per provider. |\n\nState is stored at `$PASEO_HOME/plugin-data/send-to-paseo/settings.json`, written\n`0600` inside a `0700` directory. It holds the token, the port, the default\nmodel, the followed profile id, the default mode, the paired flag, the last 20\nsends, the declared `externalBridgeUrl`, connected Additional Paseo machines with their server IDs\nand tokens, two optional advanced allowlists —\n`allowedExtensionIds` and `allowedHosts` (see [Security model](#security-model))\n— and `daemonPassword`.\n\n### A daemon that requires a password\n\nA daemon whose `listen` is not loopback turns on `daemon.auth.password`, and it\nthen refuses an unauthenticated connection outright. Without the password the\nplugin lists no providers and no modes, reports the daemon unreachable, and\nevery send fails. Measured against a daemon on `0.0.0.0:6767`:\n\n| Probe | Result |\n| --- | --- |\n| `GET /api/health` | `200` — unauthenticated, so liveness is decided here |\n| `GET /api/status` | `401 Unauthorized` |\n| SDK WebSocket, no password | closed with `Password required` |\n\n`daemon.auth.password` in Paseo's `config.json` is a **bcrypt hash**, so the\nplaintext cannot be derived from it and has to be supplied. Four sources, in\norder:\n\n1. `SEND_TO_PASEO_DAEMON_PASSWORD`\n2. `PASEO_PASSWORD` — the standard Paseo variable, the same one the `paseo` CLI reads\n3. `~/paseo-hub/secrets/daemon-password` — the shared Paseo VM secret convention\n4. `\"daemonPassword\": \"…\"` in the plugin's own `settings.json`\n\n**Prefer the VM secret file where that convention exists; use plugin settings\nelsewhere.** The plugin subprocess inherits the daemon's environment, which is\nfixed at daemon start, so a new env var would need a daemon restart — and that\nkills running agents. Either file is picked up by\n`paseo plugin reload send-to-paseo`. Surrounding whitespace in the VM secret\nfile is ignored.\n\nThe plugin-owned settings file is `0600` and has no password UI. The VM secret\nfile is read but never modified. Password values are never logged, echoed into\nan error, or included in a status payload.\n\nNote that on such a daemon the `paseo` CLI needs the password too, so\n`PASEO_PASSWORD` has to be set in your shell before `paseo plugin reload` will\nwork at all.\n\n`defaultProfileId`, `defaultModeId`, `externalBridgeUrl`, `additionalMachines` and `allowedHosts` are read with schema\ndefaults, so a `settings.json` written before those fields existed still\nvalidates. That matters more than it looks: a failed parse regenerates the file,\nand the file holds the pairing token — an upgrade must not silently unpair the\nextension.\n\n### Connect Primary and Additional Paseo machines\n\nThe **Paseo machines** card is populated automatically from the app's configured hosts. It keeps\noffline hosts visible and correlates a saved browser route only by exact `serverId`; labels and URLs\nare never treated as identity. The per-row access check reacquires\n`getPaseoClient(serverId)` for every action, as required when Paseo replaces a connection, and\nunknown/disconnected errors are shown without falling back to the selected or local host.\n\nAutomatic discovery is not browser routing. Host summaries intentionally contain no URL or\ncredential, and the host-targeted API is client-side. The connection code below is therefore kept\nonly to authorize the Primary plugin's server-side bridge to proxy extension requests.\n\nThe listener is loopback-only, by design. Use these exact roles:\n\n- **Browser machine:** runs Chrome.\n- **Primary Paseo machine:** sits beside Chrome; its plugin is the only bridge the extension uses.\n- **Additional Paseo machine:** a dev VM or other Paseo installation reached by the Primary plugin.\n\nOn the Additional Paseo machine, open **Share this Paseo machine**, press **Enable private\naccess**, then **Copy connection code**. On the Primary Paseo machine, paste it into **Paseo\nmachines** and press **Connect additional machine**. The code bundles the `serverId`, private URL,\nmachine name and token; it is a secret. The Primary plugin checks the other bridge's identity,\nname and contract before resolving or sending, and never follows that machine's own routes, which\nprevents routing loops.\nThe ping and operation share one end-to-end deadline: 9 seconds for resolve and 55 seconds for\nsend. Those budgets sit just inside the extension's 10-second and 60-second limits. A slow\nworktree creation therefore has time to finish without the Primary falsely reporting HTTP 503\nafter the Additional machine has already started the agent.\n\nThe button runs Tailscale Serve on the machine where it is pressed. Serve proxies a private\ntailnet HTTPS URL to the loopback port and survives the terminal closing.\n\n<details>\n<summary>Advanced: configure private access manually</summary>\n\nIf the automatic button cannot find Tailscale, run this **on the Additional Paseo machine**:\n\n```sh\n# on this Paseo machine; --bg survives the terminal closing\ntailscale serve --bg 7788\n```\n\nCopy the printed `https://…ts.net` origin into **Private bridge address** in the same plugin\nsurface and save it. Then copy the connection code and paste it on the Primary Paseo machine.\nThe first Serve command may ask you to enable tailnet HTTPS. Use\n`tailscale serve off` to disable it. Use Serve, not Funnel: Funnel is public.\nUse the URL Serve prints, not a cloud `.internal` hostname or a `100.x` address\nover plain HTTP.\n\nThe URL field accepts only an HTTPS origin, with no credentials, path, query or fragment. Saving it\nupdates the exact Host-header allowlist immediately; it does not rebind the bridge or create a proxy.\n\n</details>\n\n<details>\n<summary>Advanced: conne"}