Coding while in Manila traffic using Claude, Linux, and GitHub

I Developed and Shipped a Web App From My Phone While Sitting in Manila Traffic

There’s a particular kind of moment that used to be lost time. You’re in a queue, or on a train, or lying in bed at 11pm, and you think of the fix. Not a big architectural rethink, but the small, obvious one. The button’s in the wrong place. The date format is wrong. The menu is missing an item.

The old options:

  • Forget it
  • Add it to the end of your task list

I want to show you a third option, because the tooling quietly got good enough that a phone is now a legitimate place to ship code from. Not “read the code.” Not “approve a PR.” Actually change the thing, and have it live on your server two minutes later.

Here’s the end-to-end pipeline I use and how to build it yourself. The best part is, it took me less that two hours to set up and it’s been worth it many times over because now I can launch features completely remotely without a laptop. I can ship small features while drinking coffee, in between sets at the gym, or while in the pickup line at the kids’ school.

DISCLAIMER: I never read most of the code, but unreviewed isn’t unverified: a test suite gates every commit, a smoke test follows every deploy, a backup precedes every migration, and the blast radius is an early-stage app on a $6 server. Change any of that and my workflow will most likely change… I would NOT recommend this pipeline for production applications and anywhere near other people’s money, health records, or authentication.


The whole pipline, in ninety seconds.

I open Claude Code in my phone’s browser. I type something like:

Add a menu item to the main menu which links to the /overview page we created earlier today.

Claude finds the relevant files, makes the change, runs the test suite, and tells me what it did. I read the summary. If it looks right, I say:

commit and push

It writes a commit message, commits, pushes to GitHub. GitHub sees the push and kicks off an Action. The Action SSHes into my server and runs my deploy script: pull, install, migrate, build, restart, verify.

About ninety seconds later the site is live with the change. I put my phone back in my pocket.

That’s it. That’s the whole thing. The rest of this post is how each stage works and how to build it.


The three pieces

You need three things, and only the third one is fiddly:

  1. Claude Code: Run claude.ai/code in Safari or Chrome and point it at your GitHub repo. It can read your code, change it, run your tests, and commit.
  2. Linux (or a server that can deploy): A single shell script that pulls the latest code and restarts the app. If you already deploy by SSHing in and running commands, you already have this — it just lives in your head instead of in a file. Write it down.
  3. GitHub: Including an Action that runs that script. This is the bridge, and it’s the part worth doing carefully.

Let’s do them in reverse order, because the bridge is where the interesting decisions are.


Step 1: Write your deploy down

Before automating anything, get the deploy into a file. Mine is deploy/update.sh and it does exactly what I used to do by hand:

#!/usr/bin/env bashset -euo pipefailecho "==> Backing up the database"cp db.sqlite3 "db.sqlite3.bak-$(date +%Y%m%d-%H%M%S)"echo "==> Pulling"git pull --ff-onlyecho "==> Backend".venv/bin/pip install -q -r requirements.txt.venv/bin/python manage.py migrate --noinputecho "==> Frontend"npm ci --silent && npm run buildecho "==> Restarting"systemctl restart myapp

Two things I’d urge on you, learned the boring way:

Back up the database before migrating. Migrations are a one-way door. When you’re deploying from a phone you are, by definition, not in a good position to recover from a bad one.

End with a smoke test, not a restart. A smoke test is the cheapest possible “is it actually alive” check you run right after deploying. Mine curls the site and confirms it gets a 200, and that the API returns 403 for an anonymous request (up, but correctly refusing strangers). Without this, “the script finished” and “the site works” are different claims, and you’ll believe the wrong one.

echo "==> Verifying"code=$(curl -s -o /dev/null -w '%{http_code}' https://example.com/)[ "$code" = "200" ] && echo "OK" || { echo "FAILED"; exit 1; }

Run it by hand a few times. When you trust it, automate it.


Step 2: The bridge, and the one security decision that matters

Now GitHub needs to be able to run that script on your server. That means giving GitHub an SSH key. And that should make you slightly uncomfortable, which is good — that discomfort is pointing at the right question.

Here’s the thing most tutorials skip. The principle you want is least privilege: give a credential the smallest amount of power that still lets it do its job. So don’t give GitHub a key that can log into your server. Give it a key that can only run your deploy script.

Make a key that exists for this and nothing else:

ssh-keygen -t ed25519 -f deploy-key -C "github-actions-deploy"

Then install the public half on your server with a forced command:

command="/srv/myapp/deploy/update.sh",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAA...your-key... github-actions-deploy

That prefix is the whole trick. It shrinks the blast radius — how much damage is possible if the credential leaks. Anyone holding that private key, GitHub or someone who steals it from GitHub, can do exactly one thing: run your deploy script. No shell. No poking around the filesystem. No tunnelling into your network.

You can verify it yourself:

ssh -i deploy-key root@example.com

You’ll see PTY allocation request failed on channel 0, then your deploy output, then the connection closes. That error is not an error. That’s the cage working.

Now put the private key into your repo’s GitHub secrets (Settings → Secrets and variables → Actions) along with your host and user, and delete your local copy. It lives in GitHub now.

The workflow itself is short:

name: Deployon:  push:    branches: [main]  workflow_dispatch:jobs:  deploy:    runs-on: ubuntu-latest    steps:      - name: Deploy over SSH        run: |          mkdir -p ~/.ssh          echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519          chmod 600 ~/.ssh/id_ed25519          ssh-keyscan ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts          ssh ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}

Note there’s no command at the end of that ssh line. There doesn’t need to be — the server already decided what this key is allowed to do.

workflow_dispatch gives you a Run workflow button, which means you can also redeploy from your phone without making a commit.


Step 3: The part that’s actually just talking

With the bridge built, the phone half is almost anticlimactic. Open Claude Code, pick your repo, describe what you want.

The thing that surprised me is how much the constraint helps. On a laptop I’ll skim a diff and convince myself it’s fine. On a phone I can’t skim — so I lean on the things that don’t require eyeballs:

Let the tests be your review. If your project has a test suite, Claude runs it and tells you the result. “204 tests pass” is a claim I can evaluate in a queue. A 200-line diff is not. If you don’t have tests yet, this is the argument for starting — not purity, just leverage.

Ask for the reasoning, not the diff. “What did you change and why” is a better phone question than “show me the code.” You’re checking judgement, not syntax.

Make it verify, not assert. There’s a real difference between “I updated the notification link” and “I loaded the page, clicked through, and the modal opened with the right task.” Ask for the second. A good agent will tell you which one it actually did — and will tell you when it couldn’t check something.


The gotchas everybody is going to call me out on.

Some changes shouldn’t auto-deploy. Anything with a database migration, I push with [skip deploy] in the commit message and deploy manually later when I can watch. Add that to your workflow:

if: "!contains(github.event.head_commit.message, '[skip deploy]')"

Two repos means two deploys. If your frontend and backend are separate repos and you push both, you get two deploys racing. A file lock in your deploy script fixes it — the second waits for the first instead of trampling it:

exec 9>/var/lock/deploy.lockflock -w 300 9 || { echo "another deploy running"; exit 1; }

Commit messages matter more now. When you’re not watching the deploy, the commit log is your only record of what changed and why. “fix stuff” was always bad. Now it’s bad and it’s your incident report.

You will still want a laptop sometimes. I’m not going to pretend otherwise. For gnarly debugging, big refactors, or anything where your code changes are high risk, get in your IDE and terminal. This pipeline is for the small, clear change. That’s most changes.

FYI: If you’re concerned about shipping code changes that you haven’t reviewed (and rightfully so), there’s an easy middle ground when a change makes you nervous: ask Claude to print the diff before it pushes. Even git diff --stat (filenames and line counts, nothing more) fits on a phone screen and catches the scariest failure, which isn’t bad code but unexpected scope. A one-line fix that touched nine files is worth a second look, and you can see that without reading a single line of it.

In practice that’s one sentence: show me the diff before you commit.


Why this is a bigger deal than it sounds

For most of software history the gap between having an idea and being able to act on it was measured in furniture. You needed a desk. That gap quietly filtered out an enormous number of small improvements… Not because they were hard, but because by the time you were sitting down you were doing something else.

Engineers call this the feedback loop: the round trip from making a change to seeing its effect. Shortening it doesn’t just make you faster. It changes what you’re willing to attempt. A five-minute fix stops needing to be justified against the overhead of sitting down. You fix the annoying thing while you’re annoyed by it, which is also the moment you understand it best.

I built an assistant app this way — Django backend, React frontend, running on a $6 droplet. Push notifications, scheduled reminders, deep links. A meaningful share of it shipped from a phone, in the gaps of other days.

The tools are here. The pipeline is maybe an afternoon to build. And then the next time you’re in a queue and think the button’s in the wrong place, you can just move it.

Leave a Reply

Your email address will not be published. Required fields are marked *