A tiny, polished file-exchange web app that bridges a terminal-only workflow with a phone browser. If you talk to an AI assistant (or anyone) over SSH, you can't hand it images/PDFs and you can't see the files it makes for you. Agent Drop fixes both directions:
- You → the agent: upload files or paste text in the app → they land on disk in
in/, where the assistant reads them straight off the filesystem. - The agent → you: the assistant drops a file in
out/→ it appears in the app with an inline viewer (images, PDFs, text/code, audio, video) plus download and delete, and your phone gets an ntfy push.
Files move over the filesystem, never through the assistant's context window — so there's no token cost and no size limit beyond disk + your reverse proxy's upload cap.
- TanStack Start (React 19 + Vite + Nitro SSR) — file-based routing and server routes for the file API.
- shadcn/ui + Tailwind v4, Sonner toasts, Lucide icons.
- Runs as a self-contained Node server (Nitro
node-serverbuild). - No database, no cloud — just two folders on disk.
phone browser ──(private network)──> reverse proxy ──> 127.0.0.1:3010 (Nitro)
│ server routes
$DROPBOX_DIR/{in,out}
you upload / paste ───────────────────────────────────► in/ (assistant reads)
you view / download ◄────────────────────────────────── out/ (assistant writes) ──► ntfy push
| Method | Route | Purpose |
|---|---|---|
GET |
/api/files?box=in|out |
List a box: {name, size, mtime, mime, kind} |
POST |
/api/upload |
Multipart multi-file upload → in/ |
POST |
/api/text |
Paste text → timestamped .md in in/ |
GET |
/api/file/{box}/{name} |
Stream a file (?dl=1 to force download) |
DELETE |
/api/file/{box}/{name} |
Delete a file |
Filenames are sanitized (no path traversal, control chars stripped, leading dots
removed) and de-duplicated with a -1, -2 suffix. See
src/server/storage.ts.
scripts/watch-outbox.mjs watches out/ (stdlib fs.watch + a 10s reconcile
safety net) and POSTs an ntfy push on each new file. The
message text goes in the request body so emoji/accents survive (HTTP headers
are latin1-only). A agentdrop <file> ["caption"] helper copies a file into out/
and the caption rides along via a hidden .{name}.caption sidecar.
There is no authentication by design. Agent Drop is meant to run on a private network boundary — a Tailscale tailnet, a VPN, or LAN — and that boundary is the lock. Do not expose it to the public internet.
pnpm install
cp ntfy.env.example ntfy.env # then edit: set a long, unguessable NTFY_TOPIC
pnpm dev # dev serverpnpm build # → .output/server/index.mjs
HOST=127.0.0.1 PORT=3010 node .output/server/index.mjsPut it behind a reverse proxy on your private network and run the outbox watcher
alongside it. Sanitized deployment examples (systemd units, an nginx vhost
snippet) live in deploy/.
All config is environment variables (see ntfy.env.example):
| Var | Default | Meaning |
|---|---|---|
DROPBOX_DIR |
/var/lib/agent-drop |
Root holding in/ and out/ |
HOST / PORT |
127.0.0.1 / 3010 |
Where the Node server binds |
NTFY_URL |
https://ntfy.sh |
ntfy server |
NTFY_TOPIC |
— | Topic your phone subscribes to (keep it secret) |
NTFY_TOKEN |
— | Optional bearer token |
DROP_URL |
http://drop.home |
URL put in the push so a tap opens the app |