The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the GoDrop listing page.
Upload a file, get a hard-to-guess URL.
A drop box your coding agents can use: one Go binary, no database, your disk.
That is the whole idea. Downloads need no token; uploads and deletes do.
Coding agents write the code, run the tests and open the pull request. Asking them for a screenshot is the cheapest way to review the branches you are never going to check out and run yourself. They can take one: they start the app, drive a browser and capture the page. Then they cannot attach it.
GitHub has no supported API for attaching an image to a pull request. The web interface takes a drag and drop, and that is the whole of it (cli/cli#4228, #1895, #12960 "critical for agentic workflows", community#28219). What is left is committing binaries into the repository, an undocumented upload endpoint that can stop working on any deploy, or a bucket with a policy, a key and a bill attached to it.
An agent that can run curl can do this instead:
The identifier carries 128 random bits and the URL needs no token to open, so
GitHub renders it inline for everyone on the thread. The file itself is on your
own disk, which is where a picture of your unreleased work belongs, and it can
delete itself: -H "X-Expires-In: 30d".
Nothing about this is specific to screenshots. It is the same three lines for a video of a flaky test, a profile, a build log too long for a comment, or a generated report. What an agent needs to do it by itself, including a skill it can install, is in For AI agents.
The script picks the right binary for your machine, verifies its SHA-256
checksum against the published SHA256SUMS, installs it, and hands over to a
setup wizard that writes your configuration, creates your first token and
checks that the internet can actually reach you.
No root anywhere. The binary goes to ~/.local/bin, the configuration to
~/.godrop, and the uploads into a docker volume, so updating and removing
GoDrop later are the work of the same account. Run it as root, or set
GODROP_BIN_DIR=/usr/local/bin, to install it for everybody instead. The one
thing that does need root, once, is being in the docker group; setup checks
that before it writes anything.
Other ways:
Every package, archive and checksum is on the releases page, and the container image is at ghcr.io/fatihbaltaci/godrop.
| Server | CLI | Notes | |
|---|---|---|---|
| Linux (amd64, arm64) | ✅ | ✅ | Where it is meant to run: .deb, .rpm, .apk, container image, systemd unit |
| macOS (Intel, Apple silicon) | ✅ | ✅ | Fine for development and small installs; no systemd, so use Docker or start it yourself |
| Windows (amd64, arm64) | ⚠️ | ✅ | The binary works and is tested in CI, but there is no service wrapper, no installer script and no firewall guidance. Take the zip from Releases |
| FreeBSD | 🔧 | 🔧 | Compiles and passes tests; no binaries published |
The suite runs on Linux, macOS and Windows for every change, and each release artefact carries signed build provenance:
Four questions, and the last one is a choice between the recommended limits and setting them yourself. Everything the answers depend on is checked before a single file is written, so a setup that cannot work says so at the start rather than at the end, and the service is started and verified here rather than left as a command to paste. The picture is there so that the first command you paste uploads a real file.
Answering "GoDrop gets one from Let's Encrypt" is all HTTPS takes: it obtains the certificate itself and renews it, so there is no proxy to install and nothing to configure. The question only offers it for a name Let's Encrypt can actually issue for, and the listen port question disappears, because serving TLS means 443.
Changing your mind later is the same command with the setting on it. The token, the uploads and every answer you do not mention stay where they are, and the service restarts into the change:
Every question shows its default and can be answered with a flag instead, so CI
and agents run the same code path without a terminal. Prompts are skipped
automatically when there is no TTY; --no-input makes that explicit:
The wizard only offers what the host can do: systemd appears on Linux, not on macOS or Windows, and the commands it prints use the right shell.
Nine endpoints, no SDK, no client library: an agent that can call curl has
everything, and GET /llms.txt describes this exact instance in plain text.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /upload | ✅ | multipart, one or more file fields |
PUT | /upload/{name} | ✅ | raw request body |
GET/HEAD | /f/{id}/{name} · /f/{id}.{ext} | download; supports Range and ETag | |
DELETE | /f/{id}/{name} · /f/{id}.{ext} | ✅ | delete |
GET | /healthz | liveness | |
GET | /readyz | readiness: is storage writable | |
GET | /stats | ✅ | file count, bytes, quota, uptime |
GET | /llms.txt · /openapi.yaml | machine-readable description | |
GET | /skill.md | the agent skill, installable by URL |
Both Authorization: Bearer <token> and X-API-Key: <token> are accepted.
An upload answers with one entry per file, in the order they were sent, and
with Location pointing at the first:
One shape whatever was sent, so nothing has to branch on how many files there
were. The identifier, the extension and therefore the media type are all in the
URL, so the response does not repeat them; the name is there because the
cosmetic part of the URL is a slug and cannot give it back. expires_at
appears only when the upload asked for one, and GODROP_RETENTION is a
maximum, so a longer request is capped at it.
Without a JSON parser to hand, the header is the whole answer:
Status codes: 201 uploaded · 204 deleted · 400 malformed body or too
many files · 401 bad token · 404 unknown id, or the name's extension does
not match the stored file · 413 file too large · 415 not multipart · 429
rate limited (honour Retry-After) · 507 quota full.
GoDrop is built to be driven by coding agents rather than by a person at a browser. Give one a base URL and a token and it can discover the rest:
--json, and in that mode prints nothing but the
document, so parsing never breakscurl and jq are the whole toolchainGoDrop ships an agent skill: the instructions a coding agent needs to upload a file and hand back a link, without being told how. Install it with GoDrop itself:
Or with the GitHub CLI, which knows where every agent keeps them:
Every running instance serves the same skill at /skill.md, so an agent that
has been given nothing but a hostname can install it without the repository or
the binary:
The skill holds no secrets. It reads GODROP_URL and GODROP_TOKEN from the
environment, so it is safe to commit alongside a project:
With those two values in its environment, an agent can finish a pull request without anyone opening a browser:
Not every agent has a shell. A desktop assistant, an IDE panel or a workflow
node can produce a chart, a report or a screenshot and then have nowhere to put
it, because it cannot run curl. For those, GoDrop serves an MCP endpoint at
/mcp, behind the same token as everything else:
Any client that takes a URL and a header works the same way, whether it is
configured through a command or through a mcpServers block of JSON.
| Tool | What it does |
|---|---|
upload_file | Stores a file and returns the public URL, with an optional expires_in |
delete_file | Removes a file, given the URL the upload returned |
storage_stats | What is stored, and the limits this instance enforces |
The endpoint is built on protocol revision 2026-07-28, the one that took the
sessions out of MCP: there is no handshake and no session identifier, every
request carries its own version and capabilities, and the server keeps nothing
between them. That is the same way the rest of GoDrop works, which is why the
endpoint is one file and not a dependency.
Clients built on the older revisions still open with a handshake, and they are answered as well: the same three tools, and still no session assigned, so there is nothing to carry, resume or tear down in either era. Both paths are checked against the official MCP Inspector.
Files travel base64 encoded inside JSON, so upload_file takes at most 16MB.
An agent that can run a shell should keep using POST /upload: it streams,
it has no size limit beyond the configured one, and it is one request either
way.
godrop mcp is the same protocol over stdin and stdout, for a client that runs
a command instead of calling a URL:
That is the whole configuration. The address and the token come from the
installation on this machine, so the client is never told a secret, and every
message is passed through to /mcp unchanged, so the tools are the same ones.
It adds one that only works there. upload_local_file takes a path and
streams the file, which means no base64, no 16MB ceiling and nothing held in
memory: a 60MB build artefact goes up in a second and comes back as a URL.
Worth setting when an agent runs unattended: without it the command can read any file the user can, which is the point of it and also the risk.
Every one of these commands, and godrop telemetry, godrop health and
godrop uninstall with them, works on the installation this machine has, not
on the shell's idea of one: on a compose deployment they run where the files
are, in the service's own container.
Tokens are stored as SHA-256 digests in <data-dir>/tokens.json (mode
0600). A leaked file cannot be turned back into a working token, and a backup
restored onto another machine keeps working, which machine-bound encryption
would break for no security gain. A running server notices new and revoked
tokens without a restart.
Give every agent, script and machine its own token: revoking one leaves the
rest alone, and LAST USED is how you find out which of them is still wired
into something.
There are two places a token can come from, and both are accepted at once.
GODROP_TOKENS in the environment is the one the service starts with: it is
what the generated .env holds, because a compose deployment has no data
directory on the host to write a file into until the container has made the
volume, and it is the only thing that works on Fly and Railway. It has no name,
so it is not a row in godrop token list; the list says where it is instead.
tokens.json is what godrop token create adds to afterwards, and the only
kind that can be revoked without a restart.
On a compose installation that file is inside the container. The commands are the same ones: GoDrop runs them where the file is, using the same binary the service runs, rather than writing a token nothing would read.
Every command documents itself. This is that output, generated from the binary so it cannot drift:
godrop --helpgodrop serve --helpgodrop init --helpgodrop token --helpgodrop token create --helpgodrop token list --helpgodrop token revoke --helpgodrop doctor --helpgodrop skill --helpgodrop skill install --helpgodrop skill show --helpgodrop update --helpgodrop telemetry --helpgodrop health --helpgodrop version --helpEverything is an environment variable. Sizes accept 100MB, 2GB, 512KB;
durations accept 30d, 12h, 90m; rates accept 60/m, 10/s, 100/h.
| Variable | Default | Meaning |
|---|---|---|
GODROP_TOKENS | (required) | Comma-separated API tokens |
GODROP_BASE_URL | (from request) | Public URL used in responses |
GODROP_ADDR | :8747 (:443 with TLS) | Listen address |
GODROP_DATA_DIR | ./data | Where files live (/data in the image) |
GODROP_TLS | off | auto for Let's Encrypt, file for your own certificate |
GODROP_TLS_DOMAINS | (from base URL) | Names to get a certificate for |
GODROP_TLS_EMAIL | (none) | Expiry warnings from Let's Encrypt |
GODROP_TLS_CACHE_DIR | <data dir>/acme | Account key and certificates |
GODROP_TLS_CERT / GODROP_TLS_KEY | (none) | Full chain and key, in PEM |
GODROP_HTTP_ADDR | :80 with TLS | Redirect and challenge listener, off to disable |
GODROP_MAX_FILE_SIZE | 100MB | Per-file limit → 413 |
GODROP_MAX_FILES_PER_REQUEST | 20 | Files per multipart request |
GODROP_MAX_TOTAL_SIZE | (unlimited) | Storage quota → 507 |
GODROP_RETENTION | (forever) | Delete uploads older than this |
GODROP_CACHE_MAX_AGE | 8760h | How long caches may hold a download; also how long a deleted one can outlive the delete. 0 forbids caching |
GODROP_RATE_LIMIT | (off) | Uploads per token |
GODROP_AUTH_RATE_LIMIT | (off) | Failed authentications per client address |
GODROP_CORS_ORIGINS | * | Browser origins allowed to call the API |
GODROP_READ_HEADER_TIMEOUT | 10s | Slow-header protection |
GODROP_READ_TIMEOUT / GODROP_WRITE_TIMEOUT | 0 | Body timeouts, off on purpose |
GODROP_IDLE_TIMEOUT | 120s | Keep-alive idle timeout |
GODROP_SHUTDOWN_TIMEOUT | 30s | Grace period for in-flight transfers |
GODROP_LOG_FORMAT / GODROP_LOG_LEVEL | json / info | Logging |
GODROP_ACCESS_LOG | true | Per-request log lines |
GODROP_TELEMETRY | on | Anonymous daily heartbeat |
The body timeouts default to
0deliberately. A 100MB file over a slow connection is legitimate and takes minutes; aWriteTimeoutwould cut it off mid-transfer. The size limit, not the clock, is what bounds an upload.
See .env.example for the annotated version.
The identifier is the index. It carries its own location, so a lookup needs no database and no directory scan; the timestamp keeps directories small and makes retention a directory-level operation; the 128 random bits make URLs unguessable. The extension is the only metadata kept. The MIME type is derived from it at download time.
There is no listing endpoint, by design. Keep the URL an upload returns.
Retention only ever deletes uploads. tokens.json and the telemetry markers
share the directory, and sweeping those away by age would revoke every token.
Run it from any shell on the server: it reads the .env setup wrote, so the
diagnosis is of your installation rather than of an empty environment. A
compose deployment keeps its files in a volume only the container can see, so
the container is asked about those and this machine answers for the network;
one report comes out of the two.
It exits non-zero when a check fails, so it works as a deployment gate:
godrop doctor --json | jq '.checks[] | select(.status=="fail")'.
The reachability check asks https://godrop.sh/api/check to fetch your
/healthz from the public internet, the only way to catch a cloud firewall,
which is invisible from inside the machine. Only the URL is sent; skip it with
--offline.
To diagnose an instance from your own machine, pass its address and a token. The token goes in the environment, not on the command line, where the process list and the shell history would both keep a copy:
Nothing is replaced until the download has been checked against the published
SHA256SUMS and the new binary has been run and seen to report its own
version, so a failed update leaves the working installation exactly as it was.
The swap itself is a rename, which is atomic: a running server keeps serving
from the binary it started with and picks up the new one when it restarts.
The service is moved on too, because the binary you update is the command line
and the service may be a container running a different copy. A compose
deployment is pulled and recreated, which replaces the container and keeps the
named volume, so the uploads and the token survive it; a systemd deployment is
restarted. Re-running the installer, or godrop init, over an installation
that already exists does exactly the same thing rather than asking the
questions again:
An installation that belongs to a package manager is refused rather than overwritten, with the command that does the job instead:
| Installed with | Update with |
|---|---|
install.sh, or a downloaded archive | godrop update |
.deb | sudo apt update && sudo apt install --only-upgrade godrop |
.rpm | sudo dnf upgrade godrop |
| Docker | docker pull ghcr.io/fatihbaltaci/godrop |
go install | go install github.com/fatihbaltaci/GoDrop/cmd/godrop@latest |
TLS is not required. GoDrop speaks plain http, and on a network where nobody can read the traffic that is the right choice rather than a compromise:
Set GODROP_BASE_URL to whatever the client will actually type, because that
is what the returned URLs are built from: http://localhost:8747,
http://100.101.102.103:8747 for a Tailscale address, or
http://nas.local:8747 on a home network. Leave it unset and the URL is
derived from the request, which also works.
godrop doctor judges plain http by who could be listening. Loopback and
Tailscale pass, because nothing readable leaves the machine in the first case
and the connection is already encrypted in the second. A LAN address warns:
tokens are readable by anything else on that network. A public address fails.
The wizard asks how you want the certificate, and "GoDrop gets one for me" is the first answer. See HTTPS below for what each answer does.
deploy/ holds a Caddy and an nginx
configuration for anyone who wants a proxy anyway, and a hardened
godrop.service for systemd. Whichever proxy you use,
raise its body size limit to match GODROP_MAX_FILE_SIZE. godrop doctor
tests this for you.
The .deb and .rpm packages do the systemd part for you: they install the
unit, create an unprivileged godrop user, and put the configuration in
/etc/godrop/godrop.env (kept across upgrades) with uploads in
/var/lib/godrop.
Create a service from this repository, add a volume mounted at /data, and
set GODROP_TOKENS and GODROP_BASE_URL. Without the volume, uploads vanish
on the next deploy.
Use render.yaml as a blueprint. The persistent disk requires a
paid instance and pins the service to one instance, because GoDrop stores files
on local disk and does not scale horizontally, by design.
GoDrop can serve https itself, so a public install needs no proxy at all:
That is the whole configuration. On the first request GoDrop gets a
certificate from Let's Encrypt, keeps it in <data dir>/acme and renews it
long before it expires.
Open 443 and 80 to the internet. Port 80 answers the certificate
challenge and redirects anyone who typed http://, so an install that opens
only 443 waits for a certificate that never arrives. On a VPS that means both
the host firewall and your provider's, which is invisible from inside the
machine:
godrop init and godrop doctor both check the two ports and say which one
is missing. If port 80 is genuinely unavailable, set GODROP_HTTP_ADDR=off
and the certificate is still issued over 443 alone, through acme-tls/1.
Already have a certificate, from certbot, your company CA or your cloud provider? Name the two files and nothing else changes:
godrop doctor then reports how many days that certificate has left, and
whether anyone else on the machine can read the key.
In Docker it is the same two variables and the two ports:
| Your situation | Setting |
|---|---|
| Public domain, nothing in front | GODROP_TLS=auto |
| A certificate you already have | GODROP_TLS_CERT and GODROP_TLS_KEY |
| Caddy, nginx, Traefik or a cloud load balancer in front | leave TLS off |
| Loopback, a LAN, Tailscale, a private network | leave TLS off |
Turning TLS on moves the listener to :443 and starts a second one on :80,
unless you set GODROP_ADDR or GODROP_HTTP_ADDR yourself. Under systemd
that needs AmbientCapabilities=CAP_NET_BIND_SERVICE, which the shipped unit
already has; in Docker, publish -p 443:443 -p 80:80.
A certificate can only be issued for a public name that resolves to this
machine. nas.local, 10.0.0.5 and a Tailscale name are all refused at
startup, with the reason, rather than failing in a retry loop afterwards.
crypto/rand; no enumeration
endpoint exists.html, .svg, .xml and friends are
always sent as downloads, with nosniff and Content-Security-Policy: default-src 'none'; sandbox on every response/f/<id>/setup.exe does not resolve to a stored .jpgGODROP_CACHE_MAX_AGE is the knob
for an operator who would rather a delete took effect everywhere quicklySECURITY.md sets out what GoDrop defends, what it deliberately does not, and a hardening checklist. Found something? Open a security advisory on GitHub rather than an issue.
GoDrop sends one anonymous heartbeat per day:
That is the entire payload: no file names, no counts, no addresses, no base URL. It exists to answer "how many installations are there, on what, and how many are stuck on an old version". Setup does not ask about it: it says what is sent, at the end, along with the command that stops it.
Binaries built from source have no telemetry key compiled in and never report.
The website lives in site/ and worker/, and is deployed
separately from the binary. See worker/README.md.
Releases come from GoReleaser via
.goreleaser.yaml: binaries, archives, checksums, Linux
packages and the changelog. CI builds the whole set on every change, so a tag
never fails on something that could have been caught earlier.
Every statement in internal/ is covered by a test, and CI fails if that ever
slips. cmd/godrop is a three-line shim around os.Exit and is excluded.
The fuzz corpus in internal/server/testdata/ includes a case that fuzzing
found: a file name that produced .. inside a URL segment.
Sponsored by
The gopher is after the Go mascot by Renée French, CC BY 3.0.