Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ claude --plugin-dir ./apps/hook

| Variable | Description |
|----------|-------------|
| `PLANNOTATOR_REMOTE` | Set to `1` or `true` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. |
| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. |
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
| `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
| `PLANNOTATOR_SHARE` | Set to `disabled` to turn off URL sharing entirely. Default: enabled. |
| `PLANNOTATOR_SHARE_URL` | Custom base URL for share links (self-hosted portal). Default: `https://share.plannotator.ai`. |
| `PLANNOTATOR_PASTE_URL` | Base URL of the paste service API for short URL sharing. Default: `https://plannotator-paste.plannotator.workers.dev`. |

**Legacy:** `SSH_TTY` and `SSH_CONNECTION` are still detected. Prefer `PLANNOTATOR_REMOTE=1` for explicit control.
**Legacy:** `SSH_TTY` and `SSH_CONNECTION` are still detected when `PLANNOTATOR_REMOTE` is unset. Set `PLANNOTATOR_REMOTE=1` / `true` to force remote mode or `0` / `false` to force local mode.

**Devcontainer/SSH usage:**
```bash
Expand Down
2 changes: 1 addition & 1 deletion apps/codex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ The message opens in the annotation UI where you can highlight text, add comment

| Variable | Description |
|----------|-------------|
| `PLANNOTATOR_REMOTE` | Set to `1` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. |
| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. |
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
| `PLANNOTATOR_BROWSER` | Custom browser to open. macOS: app name or path. Linux/Windows: executable path. |

Expand Down
2 changes: 1 addition & 1 deletion apps/copilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ When you use plan mode in Copilot CLI:

| Variable | Description |
|----------|-------------|
| `PLANNOTATOR_REMOTE` | Set to `1` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. |
| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. |
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
| `PLANNOTATOR_BROWSER` | Custom browser to open. macOS: app name or path. Linux/Windows: executable path. |
| `PLANNOTATOR_SHARE` | Set to `disabled` to turn off URL sharing. |
Expand Down
6 changes: 3 additions & 3 deletions apps/hook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,14 @@ When Claude Code calls `ExitPlanMode`, this hook intercepts and:

| Variable | Description |
|----------|-------------|
| `PLANNOTATOR_REMOTE` | Set to `1` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. |
| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. |
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
| `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
| `PLANNOTATOR_SHARE_URL` | Custom share portal URL for self-hosting. Default: `https://share.plannotator.ai`. |

## Remote / Devcontainer Usage

When running Claude Code in a remote environment (SSH, devcontainer, WSL), set these environment variables:
When running Claude Code in a remote environment (SSH, devcontainer, WSL), set `PLANNOTATOR_REMOTE=1` (or `true`) and these environment variables:

```bash
export PLANNOTATOR_REMOTE=1
Expand All @@ -91,7 +91,7 @@ export PLANNOTATOR_PORT=9999 # Choose a port you'll forward

This tells Plannotator to:
- Use a fixed port instead of a random one (so you can set up port forwarding)
- Skip auto-opening the browser (since you'll open it manually on your local machine)
- Use remote-friendly port/browser handling for forwarded environments
- Print the URL to the terminal for you to access

**Port forwarding in VS Code devcontainers:** The port should be automatically forwarded. Check the "Ports" tab.
Expand Down
2 changes: 1 addition & 1 deletion apps/hook/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
* --browser <name> - Override which browser to open (e.g. "Google Chrome")
*
* Environment variables:
* PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode (preferred)
* PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
*/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Plannotator is configured through environment variables and hook/plugin configur

| Variable | Default | Description |
|----------|---------|-------------|
| `PLANNOTATOR_REMOTE` | auto-detect | Set to `1` or `true` to force remote mode. Uses a fixed port and skips browser auto-open. |
| `PLANNOTATOR_REMOTE` | auto-detect | Set to `1` or `true` to force remote mode, `0` or `false` to force local mode, or leave unset to auto-detect via `SSH_TTY` / `SSH_CONNECTION`. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. |
| `PLANNOTATOR_PORT` | random (local) / `19432` (remote) | Fixed server port. Useful for port forwarding in remote environments. |
| `PLANNOTATOR_BROWSER` | system default | Custom browser or script to open the UI. |
| `PLANNOTATOR_SHARE` | enabled | Set to `disabled` to turn off URL sharing entirely. |
Expand Down Expand Up @@ -65,7 +65,7 @@ Approved and denied plans are saved to `~/.plannotator/plans/` by default. You c

## Remote mode

When working over SSH, in a devcontainer, or in Docker, set `PLANNOTATOR_REMOTE=1` and `PLANNOTATOR_PORT` to a port you'll forward. See the [remote & devcontainers guide](/docs/guides/remote-and-devcontainers/) for setup instructions.
When working over SSH, in a devcontainer, or in Docker, set `PLANNOTATOR_REMOTE=1` (or `true`) and `PLANNOTATOR_PORT` to a port you'll forward. Set `PLANNOTATOR_REMOTE=0` / `false` if you need to force local behavior even when SSH env vars are present. See the [remote & devcontainers guide](/docs/guides/remote-and-devcontainers/) for setup instructions.

## Custom browser

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ sidebar:
section: "Guides"
---

Plannotator works in remote environments — SSH sessions, VS Code Remote, devcontainers, and Docker. The key difference is that the browser can't auto-open on a headless server, so you need a fixed port and manual URL access.
Plannotator works in remote environments — SSH sessions, VS Code Remote, devcontainers, and Docker. The key difference is that remote sessions benefit from a fixed port for forwarding, and browser-opening behavior depends on your environment.

## Remote mode

Set `PLANNOTATOR_REMOTE=1` to enable remote mode:
Set `PLANNOTATOR_REMOTE=1` (or `true`) to force remote mode:

```bash
export PLANNOTATOR_REMOTE=1
Expand All @@ -20,11 +20,11 @@ export PLANNOTATOR_PORT=9999 # Choose a port you'll forward
Remote mode changes two behaviors:

1. **Fixed port** — Uses `PLANNOTATOR_PORT` (default: `19432`) instead of a random port, so you can set up port forwarding once
2. **No browser auto-open** — Prints the URL to the terminal instead of trying to open a browser
2. **Browser handling changes** — In headless setups you may need to open the forwarded URL manually instead of relying on browser auto-open

### Legacy detection

Plannotator also detects `SSH_TTY` and `SSH_CONNECTION` environment variables for automatic remote mode. However, `PLANNOTATOR_REMOTE=1` is preferred for explicit control.
Plannotator also detects `SSH_TTY` and `SSH_CONNECTION` environment variables for automatic remote mode when `PLANNOTATOR_REMOTE` is unset. Use `PLANNOTATOR_REMOTE=1` / `true` to force remote mode or `PLANNOTATOR_REMOTE=0` / `false` to force local mode.

## VS Code Remote / devcontainers

Expand Down Expand Up @@ -62,7 +62,7 @@ Or forward ad-hoc when connecting:
ssh -L 9999:localhost:9999 your-server
```

Then open `http://localhost:9999` locally when Plannotator prints the URL.
Then open `http://localhost:9999` locally if Plannotator does not open a browser for you.

## Docker (without VS Code)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ All Plannotator environment variables and their defaults.

| Variable | Default | Description |
|----------|---------|-------------|
| `PLANNOTATOR_REMOTE` | auto-detect | Set to `1` or `true` to force remote mode. Uses fixed port and skips browser auto-open. |
| `PLANNOTATOR_REMOTE` | auto-detect | Set to `1` or `true` to force remote mode, `0` or `false` to force local mode, or leave unset to auto-detect via `SSH_TTY` / `SSH_CONNECTION`. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. |
| `PLANNOTATOR_PORT` | random (local) / `19432` (remote) | Fixed server port. When not set, local sessions use a random port; remote sessions default to `19432`. |
| `PLANNOTATOR_BROWSER` | system default | Custom browser to open the UI in. macOS: app name or path. Linux/Windows: executable path. Can also be a script. Takes priority over `BROWSER`. Also settable per-invocation with `--browser`. |
| `BROWSER` | (none) | Standard env var for specifying a browser. VS Code sets this automatically in devcontainers. Used as fallback when `PLANNOTATOR_BROWSER` is not set. |
Expand Down Expand Up @@ -46,11 +46,11 @@ When running your own paste service binary, these variables configure it:

## Remote mode behavior

When `PLANNOTATOR_REMOTE=1` or SSH is detected:
When remote mode is forced with `PLANNOTATOR_REMOTE=1` / `true`, or SSH is detected while `PLANNOTATOR_REMOTE` is unset:

- Server binds to `PLANNOTATOR_PORT` (default `19432`) instead of a random port
- Browser auto-open is skipped
- The URL is printed to stderr for manual access
- Browser-opening behavior depends on the environment and configured browser handler
- In headless setups, you may need to open the forwarded URL manually

### Legacy SSH detection

Expand All @@ -61,7 +61,7 @@ These environment variables are still detected for backwards compatibility:
| `SSH_TTY` | Set by SSH when a TTY is allocated |
| `SSH_CONNECTION` | Set by SSH with connection details |

If either is present, Plannotator enables remote mode automatically. Prefer `PLANNOTATOR_REMOTE=1` for explicit control.
If either is present, Plannotator enables remote mode automatically when `PLANNOTATOR_REMOTE` is unset. Set `PLANNOTATOR_REMOTE=1` / `true` to force remote mode or `0` / `false` to force local mode.

## Port resolution order

Expand Down
4 changes: 2 additions & 2 deletions apps/opencode-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Restart OpenCode. The `submit_plan` tool is now available.

| Variable | Description |
|----------|-------------|
| `PLANNOTATOR_REMOTE` | Set to `1` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. |
| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. |
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
| `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
| `PLANNOTATOR_SHARE_URL` | Custom share portal URL for self-hosting. Default: `https://share.plannotator.ai`. |
Expand All @@ -76,7 +76,7 @@ Works in containerized environments. Set the env vars and forward the port:
}
```

Then open `http://localhost:9999` when `submit_plan` is called.
If nothing opens automatically, open `http://localhost:9999` when `submit_plan` is called.

See [devcontainer.md](./devcontainer.md) for full setup details.

Expand Down
6 changes: 3 additions & 3 deletions apps/opencode-plugin/devcontainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Add these to your `devcontainer.json`:

| Variable | Purpose |
|----------|---------|
| `PLANNOTATOR_REMOTE=1` | Tells Plannotator not to open a browser (required in containers) |
| `PLANNOTATOR_REMOTE=1` | Forces remote mode for container-friendly port/browser handling (required in containers) |
| `PLANNOTATOR_PORT=9999` | Fixed port for the UI (required for port forwarding) |

Both are required. Just setting the port isn't enough.
Expand All @@ -35,7 +35,7 @@ Ensure port 9999 (or your chosen port) is forwarded to your host. In VS Code dev
4. Open `http://localhost:9999` in your host browser
5. Approve or deny the plan

**Note:** There's no browser auto-open or notification in remote mode. You'll need to manually navigate to the URL when you see the agent call `submit_plan`.
**Note:** Browser opening depends on your container/browser setup. If nothing opens automatically, navigate to the forwarded URL manually when you see the agent call `submit_plan`.

## OpenCode Web

Expand All @@ -49,7 +49,7 @@ Ensure port 9999 (or your chosen port) is forwarded to your host. In VS Code dev

## Legacy Support

If your environment already has `SSH_TTY` or `SSH_CONNECTION` set (common in SSH sessions), Plannotator will detect remote mode automatically. The `PLANNOTATOR_REMOTE` env var is preferred for Docker/devcontainer setups where those aren't present.
If your environment already has `SSH_TTY` or `SSH_CONNECTION` set (common in SSH sessions), Plannotator will detect remote mode automatically when `PLANNOTATOR_REMOTE` is unset. You can also force local mode with `PLANNOTATOR_REMOTE=false` or `0`.

## Troubleshooting

Expand Down
2 changes: 1 addition & 1 deletion apps/opencode-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* revisions and resubmit with the file path.
*
* Environment variables:
* PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode (devcontainer, SSH)
* PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
* PLANNOTATOR_PLAN_TIMEOUT_SECONDS - Max wait for approval (default: 345600, set 0 to disable)
* PLANNOTATOR_ALLOW_SUBAGENTS - Set to "1" to allow subagents to see submit_plan
Expand Down
96 changes: 96 additions & 0 deletions apps/pi-extension/server/network.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { afterEach, describe, expect, test } from "bun:test";
import { getServerPort, isRemoteSession } from "./network";

const savedEnv: Record<string, string | undefined> = {};
const envKeys = ["PLANNOTATOR_REMOTE", "PLANNOTATOR_PORT", "SSH_TTY", "SSH_CONNECTION"];

function clearEnv() {
for (const key of envKeys) {
savedEnv[key] = process.env[key];
delete process.env[key];
}
}

afterEach(() => {
for (const key of envKeys) {
if (savedEnv[key] !== undefined) {
process.env[key] = savedEnv[key];
} else {
delete process.env[key];
}
}
});

describe("pi remote detection", () => {
test("false by default", () => {
clearEnv();
expect(isRemoteSession()).toBe(false);
});

test("true when PLANNOTATOR_REMOTE=1", () => {
clearEnv();
process.env.PLANNOTATOR_REMOTE = "1";
expect(isRemoteSession()).toBe(true);
});

test("true when PLANNOTATOR_REMOTE=true", () => {
clearEnv();
process.env.PLANNOTATOR_REMOTE = "true";
expect(isRemoteSession()).toBe(true);
});

test("false when PLANNOTATOR_REMOTE=0", () => {
clearEnv();
process.env.PLANNOTATOR_REMOTE = "0";
expect(isRemoteSession()).toBe(false);
});

test("false when PLANNOTATOR_REMOTE=false", () => {
clearEnv();
process.env.PLANNOTATOR_REMOTE = "false";
expect(isRemoteSession()).toBe(false);
});

test("PLANNOTATOR_REMOTE=false overrides SSH_TTY", () => {
clearEnv();
process.env.PLANNOTATOR_REMOTE = "false";
process.env.SSH_TTY = "/dev/pts/0";
expect(isRemoteSession()).toBe(false);
});

test("PLANNOTATOR_REMOTE=0 overrides SSH_CONNECTION", () => {
clearEnv();
process.env.PLANNOTATOR_REMOTE = "0";
process.env.SSH_CONNECTION = "192.168.1.1 12345 192.168.1.2 22";
expect(isRemoteSession()).toBe(false);
});

test("true when SSH_TTY is set and env var is unset", () => {
clearEnv();
process.env.SSH_TTY = "/dev/pts/0";
expect(isRemoteSession()).toBe(true);
});
});

describe("pi port selection", () => {
test("uses random local port when false overrides SSH", () => {
clearEnv();
process.env.PLANNOTATOR_REMOTE = "false";
process.env.SSH_TTY = "/dev/pts/0";
expect(getServerPort()).toEqual({ port: 0, portSource: "random" });
});

test("uses default remote port when SSH is detected", () => {
clearEnv();
process.env.SSH_CONNECTION = "192.168.1.1 12345 192.168.1.2 22";
expect(getServerPort()).toEqual({ port: 19432, portSource: "remote-default" });
});

test("PLANNOTATOR_PORT still takes precedence", () => {
clearEnv();
process.env.PLANNOTATOR_REMOTE = "false";
process.env.SSH_TTY = "/dev/pts/0";
process.env.PLANNOTATOR_PORT = "9999";
expect(getServerPort()).toEqual({ port: 9999, portSource: "env" });
});
});
25 changes: 21 additions & 4 deletions apps/pi-extension/server/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,30 @@ const DEFAULT_REMOTE_PORT = 19432;

/**
* Check if running in a remote session (SSH, devcontainer, etc.)
* Honors PLANNOTATOR_REMOTE env var, or detects SSH_TTY/SSH_CONNECTION.
* Honors PLANNOTATOR_REMOTE as a tri-state override, or detects SSH_TTY/SSH_CONNECTION.
*/
function isRemoteSession(): boolean {
function getRemoteOverride(): boolean | null {
const remote = process.env.PLANNOTATOR_REMOTE;
if (remote === undefined) {
return null;
}

if (remote === "1" || remote?.toLowerCase() === "true") {
return true;
}

if (remote === "0" || remote?.toLowerCase() === "false") {
return false;
}

return null;
}

export function isRemoteSession(): boolean {
const remoteOverride = getRemoteOverride();
if (remoteOverride !== null) {
return remoteOverride;
}
// Legacy SSH detection
if (process.env.SSH_TTY || process.env.SSH_CONNECTION) {
return true;
Expand All @@ -32,7 +49,7 @@ function isRemoteSession(): boolean {
* - Local sessions use random port
* Returns { port, portSource } so caller can notify user if needed.
*/
function getServerPort(): {
export function getServerPort(): {
port: number;
portSource: "env" | "remote-default" | "random";
} {
Expand Down Expand Up @@ -98,7 +115,7 @@ export async function listenOnPort(

/**
* Open URL in system browser (Node-compatible, no Bun $ dependency).
* Honors PLANNOTATOR_BROWSER and BROWSER env vars, matching packages/server/browser.ts.
* Honors PLANNOTATOR_BROWSER and BROWSER env vars.
* Returns { opened: true } if browser was opened, { opened: false, isRemote: true, url } if remote session.
*/
export function openBrowser(url: string): {
Expand Down
2 changes: 1 addition & 1 deletion apps/portal/ANNOTATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,6 @@ The annotate server respects the same environment variables as plan review:

| Variable | Description |
|----------|-------------|
| `PLANNOTATOR_REMOTE` | Set to `1` for remote/SSH mode (fixed port, no browser open) |
| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection (fixed port in remote mode; browser behavior depends on the environment) |
| `PLANNOTATOR_PORT` | Fixed port (default: random locally, `19432` for remote) |
| `PLANNOTATOR_BROWSER` | Custom browser to open the UI in |
4 changes: 2 additions & 2 deletions apps/review/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* bun apps/review/server/index.ts HEAD~5..HEAD # Commit range
*
* Environment variables:
* PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode
* PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
*/

Expand Down Expand Up @@ -70,7 +70,7 @@ const server = await startReviewServer({
handleReviewServerReady(url, isRemote, port);
console.error(`Code review at ${url}`);
if (isRemote) {
console.error(`(Remote mode - manually open the URL above)`);
console.error(`(Remote mode detected — if no browser opens automatically, use the URL above)`);
}
},
});
Expand Down
2 changes: 1 addition & 1 deletion packages/server/annotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* render it without modifications.
*
* Environment variables:
* PLANNOTATOR_REMOTE - Set to "1" or "true" for remote/devcontainer mode
* PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
*/

Expand Down
Loading