Most of my side projects start the same way: I'm on my phone, talking an idea through with Claude, and at
some point the conversation is good enough that I want to try it. Then the momentum dies. I have to get to
a laptop, SSH into my server, make a folder, start Claude Code, and paste in a summary of everything we just
discussed.
So I built stairway: a small MCP server that lets the chat do that
step itself.
"Ok, let's try it. Create an experiment called
rss-digestand brief it with what we said."
A few seconds later there's a folder on my server, a git repo in it, and a Claude Code session already working
on the brief. The session shows up in claude.ai/code, so I can follow it from my phone.
What it does
stairway is one Python file, standard library only (Python 3.8+), about 1,700 lines. It exposes these MCP
tools:
-
create_experiment(name, prompt)creates<root>/<name>, runsgit init, and startsclaude --remote-control <name> "<prompt>"inside a detachedscreen. -
send_prompt(name, prompt)passes more instructions to that running session, mid-conversation. -
attach_folder(path)lets Claude use an existing project, but only after I press Approve on stairway's status page. -
experiment_status,open_editor,stop_experiment,forget_experimentand friends.
Around those tools there are three ways to work on an experiment:
- claude.ai/code, because every session runs with remote control on.
- A browser VS Code, started on demand per experiment and proxied by stairway. The first terminal joins the Claude session, and every other terminal is a normal shell in the folder.
-
A Telegram group. Add the bot, approve the group,
/new picnic-planner <brief>, and from then on you talk to Claude by mentioning the bot, while the humans discuss what it builds. Its replies, permission requests and files come back to the group.
The parts I found interesting
An OAuth 2.1 server with no storage
My first version used a token in the URL. claude.ai reached the server and got a valid initialize
response… and then went looking for OAuth anyway (/.well-known/oauth-protected-resource, dynamic client
registration at /register), and gave up when those returned 404.
So stairway is its own small OAuth 2.1 authorization server: discovery documents, dynamic client
registration, PKCE (S256), refresh tokens. The twist is that nothing is stored. Client IDs, access tokens
and refresh tokens are all HMAC-signed blobs:
def _sign(payload):
body = _b64(json.dumps(payload, separators=(",", ":")).encode())
return body + "." + _b64(hmac.new(TOKEN.encode(), b"oauth." + body.encode(), hashlib.sha256).digest())
A registered client is just {"k": "client", "r": [redirect_uris], "n": name}, signed. Only the short-lived
authorization codes live in memory. As a bonus, rotating the one server secret revokes every grant at once.
Typing into a TUI, reliably
claude --remote-control gives you an interactive session that's also visible on claude.ai. To pass it more
prompts, stairway types into the screen it runs in. A naive screen -X stuff "text\r" breaks on multi-line
prompts, since each newline submits. It also mangles ^ and \.
The fix is a bracketed paste: load the text into screen's paste buffer from a file, wrapped in
ESC[200~ … ESC[201~, paste it, then press Enter.
f.write("\033[200~" + prompt + "\033[201~")
subprocess.run(["screen", "-S", session, "-X", "readbuf", "-e", "utf8", path])
subprocess.run(["screen", "-S", session, "-X", "paste", "."])
_stuff(session, "\r")
Claude Code sees it as one paste, multi-line and with every character intact, and queues it if it's busy.
Getting answers out: Claude Code hooks
For Telegram I needed Claude's replies, and scraping a TUI is miserable. Claude Code's hooks solve this. The
Stop hook payload includes last_assistant_message, so each session starts with:
{"hooks": {
"Stop": [{"hooks": [{"type": "command", "command": "curl -s --data-binary @- 'http://127.0.0.1:8765/hooks/<name>/stop?k=<key>'"}]}],
"Notification": [{"hooks": [{"type": "command", "command": "curl -s --data-binary @- '…/notify?k=<key>'"}]}]
}}
stairway relays the reply to the bound Telegram group, as a reply to the message that asked. Permission
prompts arrive as "⏳ needs your permission", which the group can answer with /approve or /deny. An
--append-system-prompt tells Claude which curl sends a file it made to the group, so charts and images
just show up there.
The red bars
Inside screen, Claude Code's UI was covered in solid red bars. Capturing the raw output showed why: Claude
draws with 24-bit colors (ESC[38;2;153;153;153m), and screen 4.x doesn't understand them. It reads 38,
2, 153, … as separate attributes, and some of those numbers mean "red background". Setting
COLORTERM or FORCE_COLOR didn't help. Starting the session with Claude Code's ANSI theme did:
--settings '{"theme": "dark-ansi"}'
It's set per process, so my own Claude Code theme stays as it is.
A browser VS Code without dependencies
code-server listens on a unix socket per experiment, and stairway proxies /code/<name>/ to it with a
deliberately dumb byte pipe. It forwards the request head, then shovels bytes both ways with select() until
either side closes. Websockets just work, and plain requests get Connection: close so a keep-alive
connection never gets mixed up between routes.
Security, honestly
stairway's purpose is to let an AI agent run commands on your machine when asked. So:
- one strong secret, OAuth with your explicit consent, and HTTPS in front
- existing folders and Telegram groups do nothing until you approve them on the status page
- choose each experiment's Claude Code
permission_modedeliberately;defaultasks before acting - run it as a dedicated user, ideally on a box you're happy to give an agent
The repo has a security doc with the full
threat model.
Try it
git clone https://github.com/sirmmo/stairway.git && cd stairway
cp .env.example .env # set STAIRWAY_TOKEN, STAIRWAY_PUBLIC_URL, ...
set -a; . ./.env; set +a; python3 stairway.py
Put it behind HTTPS and add https://<host>/mcp as a custom connector in claude.ai. Then ask Claude to
create your first experiment. You'll need Claude Code logged in with a claude.ai subscription on the server,
because remote control requires it. GNU screen is required, and code-server and a Telegram bot token are
optional.
I built it in a morning of pair-programming with Claude Code, which was a fitting way to make a tool for
starting Claude Code sessions. It's MIT licensed. Issues and PRs are welcome:
github.com/sirmmo/stairway.
What would you want your chat to be able to spin up?
Top comments (0)