Web-based terminal accessible from phone via Cloudflare Tunnel
Project description
Hand a computer to an agent, full control, and watch it.
One command, one URL. (Also a slick terminal for your own phone.)
1. uvx ptn
2. Hand the URL to an AI agent, or scan the QR yourself
3. Watch it work in any browser, and take over anytime
[!WARNING] That URL is full access to this computer. Anyone (or any AI agent) you hand it to gets a real shell on your machine, no password. That's the whole point, but it's dangerously easy to give an agent more than you meant to. Treat the URL like a secret, only share it with people and agents you trust, and read Security before you point it at anything important.
Why
I want to control my swarm anywhere.
ngrok requires registration and the free tier sucks. Cloudflare Quick Tunnel works great but is hard to use directly on the phone. Termius requires complicated setup: port forwarding, firewall rules, key management... Tried Claude Code web, but it can't access my local hardware and environment. Also tried Happy, but it's too bulky and updates lag behind.
So I built something simpler: run a command, scan a QR, start typing.
Then it clicked: the same trick (one command, one URL) is the easiest way to give an AI agent a real terminal on any computer. No MCP server to write, no SSH keys, no Docker, no config. Run uvx ptn, hand over the URL, and the agent runs commands, reads the screen, and answers prompts on that machine. And because it's a web terminal, you can open the same session in any browser to watch it work live, or grab the keyboard and take over.
Features
- Hand a computer to an agent, full control, and watch it - Give an AI agent the URL and it gets a real terminal on the machine via MCP or plain REST. Open the same session in any browser to watch it work live, and grab the keyboard whenever you want. No keys, no Docker. The agent learns how from
/llms.txtand/.well-known/mcp.json. See Agent access. - One command, instant access -
uvx ptnand you (or an agent) get a real terminal on this machine. No SSH, no port forwarding, no config files. Cloudflare tunnel + QR code. - Actually usable on mobile - Touch-optimized with momentum scrolling, pinch-to-zoom, swipe gestures, and modifier keys (Ctrl, Alt).
- Full terminal apps - vim, htop, less, tmux all work correctly with proper alt-screen buffer handling.
- Persistent multi-tab sessions - Sessions survive disconnects. Close the browser, switch networks, reconnect from another device, and your shell and running processes are still there. You and an agent can share one session: watch it work, or take over.
- Cross-platform - Windows (PowerShell, CMD, WSL), Linux/macOS (Bash, Zsh, Fish, Nushell, and any shell via
$SHELL). Auto-detects your shells. - Private by default - The secret tunnel URL never sits on screen, so it's safe to screen-share or screenshot the QR. Press
cto copy agent-ready sharing instructions, orufor the bare URL.
Install
| Method | Install | Update |
|---|---|---|
| uvx (no install) | uvx ptn |
uvx --refresh ptn |
| uv tool | uv tool install ptn |
uv tool upgrade ptn |
| pipx | pipx install ptn |
pipx upgrade ptn |
| pip | pip install ptn |
pip install -U ptn |
One-line install (uv + ptn):
| OS | Command |
|---|---|
| Windows | powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/lyehe/porterminal/master/install.ps1 | iex" |
| macOS/Linux | curl -LsSf https://raw.githubusercontent.com/lyehe/porterminal/master/install.sh | sh |
Requires Python 3.12+ and cloudflared (auto-installed if missing).
Usage
ptn # Start in current directory
ptn ~/projects/myapp # Start in specific folder
| Flag | Description |
|---|---|
-n, --no-tunnel |
Local network only (no Cloudflare tunnel) |
-b, --background |
Run in background and return immediately |
-p, --password |
Prompt for password to protect this session |
-sp, --save-password |
Save or clear password in config |
-tp, --toggle-password |
Set password requirement (on/off/toggle) |
-v, --verbose |
Show detailed startup logs |
-i, --init |
Create .ptn/ptn.yaml with auto-discovered project scripts as buttons |
-if, --init-from URL/PATH |
Create .ptn/ptn.yaml from a URL or local file |
-c, --compose |
Enable compose mode by default |
-k, --keep-qr |
Keep the QR code visible after the first connection |
-u, --check-update |
Check if a newer version is available |
-V, --version |
Show version |
While running: with a tunnel active, the connection URL is hidden on screen for privacy. Press c to copy a short agent-ready share message containing the URL, /mcp, /api/agent/run, and /llms.txt; press u to copy only the bare URL; or scan the QR to connect. Ctrl+C stops the server.
Agent access (MCP + REST)
The same URL also works for AI agents. MCP-capable clients can use <url>/mcp (Streamable HTTP) for native typed tools. Agents that cannot register an MCP server can use the REST fallback at <url>/api/agent/run with ordinary HTTP requests. Either path creates a persistent agent shell, shown as a 🤖 tab you can watch and take over from your phone.
Hand the agent the tunnel URL. MCP clients can auto-discover the server from <url>/.well-known/mcp.json (the MCP server.json descriptor), and there's a human/agent-readable <url>/llms.txt with usage. The base page also includes accessibility-visible hints for browser-driving agents, while the human UI stays compact. Example client config:
{
"mcpServers": {
"porterminal": { "url": "https://<your-tunnel>.trycloudflare.com/mcp" }
}
}
MCP tools: run_command (clean output + exit code), read_screen, send_keys, send_signal (Ctrl-C / EOF).
REST fallback:
curl -s -X POST https://<your-tunnel>.trycloudflare.com/api/agent/run \
-H "content-type: application/json" \
-d '{"command":"echo hello","timeout":30}'
The response includes a session_id; reuse it with /api/agent/screen, /api/agent/keys, /api/agent/signal, and DELETE /api/agent/session.
When you open Porterminal on your phone, the top-right copy button copies the same agent-ready share text. Browser-only agents also get a fallback on the base page: a DOM-readable Terminal screen mirror and a clearly labeled Terminal input.
Security: like the human terminal, the only protection is the secret tunnel URL: anyone (or any agent) with it gets full, non-elevated shell access. If you need real auth, use a different tool. See docs/agent-access.md.
Mobile Gestures
| Gesture | Action |
|---|---|
| Tap | Focus terminal, clear selection |
| Long-press | Start text selection |
| Double-tap | Select word |
| Swipe left/right | Arrow keys (← →) |
| Scroll | Momentum scrolling with physics |
| Pinch | Zoom text (10-24px) |
Modifier keys (Ctrl, Alt, Shift): Tap once for sticky (one keystroke), double-tap for lock.
Compose mode (▤ button): Toggle a text input field where you can type or dictate, edit your text with full mobile editing features (autocorrect, suggestions, cursor positioning), then send to terminal. Useful for longer commands or voice input.
Configuration
Run ptn --init to create a starter config. It auto-discovers project scripts from package.json, pyproject.toml, or Makefile and adds them as buttons:
ptn -i
# Created: .ptn/ptn.yaml
# Discovered 3 project script(s): build, dev, test
Or create ptn.yaml manually:
# Terminal settings
terminal:
default_shell: nu # Default shell ID
shells: # Custom shell definitions
- id: nu
name: Nushell
command: nu
args: []
# Custom buttons (appear in toolbar)
# row: 1 = default row, 2+ = additional rows
buttons:
- label: "claude"
send:
- "claude"
- 100 # delay in ms
- "\r"
- label: "build"
send: "npm run build\r"
row: 2 # second button row
# Update checker settings
update:
notify_on_startup: true # Show update notification
check_interval: 86400 # Seconds between checks (default: 24h)
# Security settings
security:
require_password: true # Always require password at startup
password_hash: "" # Saved password hash (use ptn -sp to set)
max_auth_attempts: 5 # Max failed attempts before disconnect
Config is searched in order: $PORTERMINAL_CONFIG_PATH, ./ptn.yaml, ./.ptn/ptn.yaml, ~/.ptn/ptn.yaml.
Security
There's no password by default. The only thing stopping anyone is that they can't guess the random tunnel URL. Anyone (or any AI agent) who gets it has a full shell on your machine. Don't expose anything you wouldn't hand to a stranger, and set a password for anything sensitive:
From the UI: Open Settings (gear icon) and use the Security section to set/change password and toggle password requirement. Changes require server restart.
From CLI:
# One-time password (prompt each session)
ptn -p
# Save password to config (no prompt needed)
ptn -sp
# Password: ****
# Confirm password: ****
# Clear saved password (enter empty password)
ptn -sp
# Password: [press Enter]
# Set or toggle password requirement
ptn -tp on # Enable
ptn -tp off # Disable
ptn -tp toggle # Toggle current state
See docs/security.md for details.
Troubleshooting
Connection fails? Cloudflare tunnel sometimes blocks connections. Restart the server (Ctrl+C, then ptn) to get a fresh tunnel URL.
Shell not detected? Set your $SHELL environment variable or configure shells in ptn.yaml.
Contributing
This project does not accept external contributions (pull requests or code changes) for security reasons (see CONTRIBUTING.md). You're welcome to fork and run your own copy under AGPL-3.0.
Run from source:
git clone https://github.com/lyehe/porterminal
cd porterminal
uv sync
uv run ptn
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ptn-1.0.1.tar.gz.
File metadata
- Download URL: ptn-1.0.1.tar.gz
- Upload date:
- Size: 241.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
455245e3270d632db05fe4e23736fe05fb093d359a347966890e204785ecd691
|
|
| MD5 |
8eb6e34b23d640de83cd401c7c4bac67
|
|
| BLAKE2b-256 |
3fc36f90d2e6c42cf44c0130a871a3941123618025987b9132c0d594a84b07b1
|
Provenance
The following attestation bundles were made for ptn-1.0.1.tar.gz:
Publisher:
publish.yml on lyehe/porterminal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ptn-1.0.1.tar.gz -
Subject digest:
455245e3270d632db05fe4e23736fe05fb093d359a347966890e204785ecd691 - Sigstore transparency entry: 1961235877
- Sigstore integration time:
-
Permalink:
lyehe/porterminal@b95523743da6c20fe9bc26399d634b4307e0f7c1 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/lyehe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b95523743da6c20fe9bc26399d634b4307e0f7c1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ptn-1.0.1-py3-none-any.whl.
File metadata
- Download URL: ptn-1.0.1-py3-none-any.whl
- Upload date:
- Size: 273.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b898954fea730fcc1fef35618344bb7f4c6da4200ca869fc7bee2fb8d49d692
|
|
| MD5 |
0ed43d10230ecb70807ada6f39ba1bfb
|
|
| BLAKE2b-256 |
740e433a679d68ca176d464f679e13a08ed3f7ff7b7a9b476c17127045feb623
|
Provenance
The following attestation bundles were made for ptn-1.0.1-py3-none-any.whl:
Publisher:
publish.yml on lyehe/porterminal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ptn-1.0.1-py3-none-any.whl -
Subject digest:
8b898954fea730fcc1fef35618344bb7f4c6da4200ca869fc7bee2fb8d49d692 - Sigstore transparency entry: 1961236031
- Sigstore integration time:
-
Permalink:
lyehe/porterminal@b95523743da6c20fe9bc26399d634b4307e0f7c1 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/lyehe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b95523743da6c20fe9bc26399d634b4307e0f7c1 -
Trigger Event:
push
-
Statement type: