{"id":51454001,"url":"https://github.com/webteractive/wormhole","last_synced_at":"2026-07-05T23:01:24.732Z","repository":{"id":362604547,"uuid":"1259952782","full_name":"webteractive/wormhole","owner":"webteractive","description":"Expose a local folder to the public internet via an ephemeral Cloudflare quick tunnel — drop a file, get a public URL, tear it down. Built for AI agent workflows.","archived":false,"fork":false,"pushed_at":"2026-06-05T02:58:52.000Z","size":37,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T04:21:26.794Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/webteractive.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-05T02:48:30.000Z","updated_at":"2026-06-05T02:58:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/webteractive/wormhole","commit_stats":null,"previous_names":["webteractive/wormhole"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/webteractive/wormhole","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webteractive%2Fwormhole","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webteractive%2Fwormhole/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webteractive%2Fwormhole/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webteractive%2Fwormhole/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/webteractive","download_url":"https://codeload.github.com/webteractive/wormhole/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webteractive%2Fwormhole/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35171814,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-05T23:01:22.995Z","updated_at":"2026-07-05T23:01:24.670Z","avatar_url":"https://github.com/webteractive.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# wormhole\n\n[![ci](https://github.com/webteractive/wormhole/actions/workflows/ci.yml/badge.svg)](https://github.com/webteractive/wormhole/actions/workflows/ci.yml)\n[![release](https://img.shields.io/github/v/release/webteractive/wormhole?sort=semver)](https://github.com/webteractive/wormhole/releases/latest)\n[![license](https://img.shields.io/github/license/webteractive/wormhole)](LICENSE)\n\n**Expose a local folder to the public internet through an ephemeral Cloudflare\nquick tunnel — drop a file, get a public URL, tear it down.**\n\n`wormhole` serves a local folder over a throwaway `*.trycloudflare.com` URL that\nlives only as long as you need it. It's a single, dependency-light Go binary and\nneeds no Cloudflare account or login.\n\n## Why\n\nLots of tools and automated workflows produce files locally — often images. Some\nconsuming services ingest media *only* by fetching a public URL; they have no\nupload endpoint. `wormhole` bridges that gap:\n\n\u003e generate a file → `wormhole put file.png` → copy the URL → hand it to the\n\u003e service → `wormhole stop`\n\nThe public host exists just long enough for the consumer to download the file,\nthen it's gone.\n\n## Features\n\n- One static binary, standard library only — no runtime to install.\n- `serve` (foreground) **and** detached `start` / `stop` / `status` with a pidfile.\n- Random subdomain **plus** a random path token, a short TTL, and clean teardown\n  (no orphaned processes).\n- A readiness probe verifies the public URL actually serves before handing it out.\n- Built to be scripting/automation-friendly: `--json` on every command, a clean\n  stdout/stderr split, and documented exit codes.\n\n## Requirements\n\n- [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)\n  on your `PATH`. Account-less quick tunnels need **no** login.\n  - macOS: `brew install cloudflared`\n  - Linux / Windows: see the download page above.\n- Nothing else at runtime. (Building from source needs Go 1.26+.)\n\n## Install\n\n### Prebuilt binary (recommended)\n\nStable \"latest\" download URLs (replace `\u003cos\u003e`/`\u003carch\u003e` with `linux`/`darwin`/`windows`\nand `amd64`/`arm64`):\n\n```\nhttps://github.com/webteractive/wormhole/releases/latest/download/wormhole_\u003cos\u003e_\u003carch\u003e.tar.gz\n```\n\nmacOS / Linux one-liner:\n\n```sh\nos=$(uname -s | tr '[:upper:]' '[:lower:]')\narch=$(uname -m); case \"$arch\" in x86_64) arch=amd64;; aarch64|arm64) arch=arm64;; esac\ncurl -fsSL \"https://github.com/webteractive/wormhole/releases/latest/download/wormhole_${os}_${arch}.tar.gz\" | tar -xz\nsudo install \"wormhole_${os}_${arch}/wormhole\" /usr/local/bin/wormhole\nwormhole --version\n```\n\nEach release also ships `checksums.txt` (SHA-256) for verification. Windows builds\nare `.zip`.\n\n### With Go\n\n```sh\ngo install github.com/webteractive/wormhole@latest\n# note: `go install` builds report version \"dev\"; released binaries embed the tag.\n```\n\n### From source\n\n```sh\ngit clone https://github.com/webteractive/wormhole\ncd wormhole\ngo build -o wormhole .\n```\n\n## Quick start\n\n```sh\n# 1. start a tunnel over a fresh temp drop dir (detached; prints the URL once live)\nwormhole start --ttl 10m\n# drop dir : /tmp/wormhole-1234\n# base url : https://random-words.trycloudflare.com/ab12cd.../\n# files    : (none yet — drop files into the drop dir)\n\n# 2. publish a file and get its public URL\nwormhole put ./image.png\n# https://random-words.trycloudflare.com/ab12cd.../image.png\n\n# 3. hand that URL to whatever needs to fetch it...\n\n# 4. tear it all down (stops the static server and cloudflared)\nwormhole stop\n```\n\nScripted / automation variant:\n\n```sh\nbase=$(wormhole start --ttl 10m --json | jq -r .base_url)\nurl=$(wormhole put ./image.png --json | jq -r .url)\n# ... use \"$url\" ...\nwormhole stop\n```\n\nYou can also point it at an existing folder and serve everything in it:\n\n```sh\nwormhole start ./my-images --ttl 30m\n```\n\n## Commands\n\n| Command | Purpose |\n| --- | --- |\n| `wormhole serve [dir] [flags]` | Foreground: print the URL and serve until Ctrl-C or TTL. |\n| `wormhole start [dir] [flags]` | Detached: print the URL once the tunnel is live, then return. |\n| `wormhole status` | Show the running instance, or `not running` (exit 6). |\n| `wormhole stop` | Stop the instance and tear everything down. |\n| `wormhole put \u003cfile\u003e [--rename name]` | Copy a file into the drop dir; print its public URL. |\n| `wormhole url \u003cfilename\u003e` | Print the public URL for a file already in the drop dir. |\n| `wormhole --version` / `--help` | Version (+ schema) / help. |\n\n`dir` defaults to a freshly created temp dir, printed so you know where to drop files.\n\n### Flags (`serve` / `start`)\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--port N` | `0` | Local listen port (`0` = pick a free port). |\n| `--ttl D` | `10m` | Lifetime before auto-teardown (e.g. `30m`, `1h`; `0` = no TTL). |\n| `--token S` | random | URL path segment. Must match `^[A-Za-z0-9_-]{1,128}$`. |\n| `--types LIST` | any | Comma-separated allowed extensions, e.g. `jpeg,png,webp,gif`. |\n| `--max-size SIZE` | none | Reject files larger than e.g. `5MB`. |\n| `--ready-timeout D` | `60s` | How long to wait for the tunnel to become reachable. |\n| `--no-verify` | off | Skip the readiness probe; publish the URL as soon as it is assigned. |\n| `--json` | off | Machine-readable output (available on every subcommand). |\n\nFlags may appear before or after the positional `dir`.\n\n## Output contract (for scripts \u0026 automation)\n\n- **stdout carries data only**; all diagnostics, progress, and cloudflared chatter\n  go to **stderr**. `wormhole start --json | jq -r .base_url` is safe.\n- `--json` emits a single JSON object with a stable schema (`wormhole/v1`):\n\n  ```json\n  {\n    \"schema\": \"wormhole/v1\",\n    \"base_url\": \"https://random-words.trycloudflare.com/ab12cd.../\",\n    \"drop_dir\": \"/tmp/wormhole-1234\",\n    \"port\": 51234,\n    \"token\": \"ab12cd...\",\n    \"ttl_seconds\": 600,\n    \"expires_at\": \"2026-01-01T12:34:56Z\",\n    \"pid\": 4242,\n    \"files\": [\n      { \"name\": \"image.png\", \"url\": \".../image.png\", \"size\": 20481, \"content_type\": \"image/png\" }\n    ]\n  }\n  ```\n\n- Errors with `--json` print `{\"schema\":\"wormhole/v1\",\"error\":{\"code\":\"...\",\"message\":\"...\"}}`\n  to stderr, alongside the matching exit code.\n\n### Exit codes\n\n| Code | Meaning |\n| --- | --- |\n| `0` | OK |\n| `2` | Usage error |\n| `3` | `cloudflared` missing |\n| `4` | Tunnel failed / not ready |\n| `5` | Already running |\n| `6` | Not running |\n| `7` | File / type / size / name rejected |\n\n## Consumer contract\n\nIf the service fetching the URL does so defensively — **redirects disabled**, the\nconnection **pinned to the resolved IP** (SSRF protection), and accepting only\n`image/jpeg`, `image/png`, `image/webp`, `image/gif` (**rejecting SVG**) —\n`wormhole` is built to satisfy it:\n\n- URLs serve the file **directly** — no redirect to a CDN.\n- The static server sends a correct `Content-Type` from the file (never an active\n  document type).\n- Keep images reasonably sized; use `--types` / `--max-size` to constrain if desired.\n\n## Security model\n\n- The `*.trycloudflare.com` subdomain is random and unguessable; files are served\n  under an additional **random path token**, so the base URL is not enumerable.\n- Only the drop dir is served — **no directory listing, no traversal, no symlink\n  escape**.\n- A short **TTL** plus explicit `stop` keep the public window small. **That window\n  is the security boundary**, not authentication. Use a short `--ttl` and stop when\n  done.\n\nAdditional hardening:\n\n- Responses always send `X-Content-Type-Options: nosniff`, and active document\n  types (`text/html`, SVG, XHTML/XML) are downgraded to `application/octet-stream`\n  so a file can't execute script on the tunnel origin if opened in a browser.\n- The public listener sets read/write/idle timeouts (slowloris-resistant).\n- State files (`wormhole.json`, `wormhole.pid`, `wormhole.log`) and the state dir\n  are owner-only (`0600` / `0700`); the log records the tokenised URL.\n- `put` / `url` reject anything that isn't a plain filename inside the drop dir, and\n  `put` won't follow or clobber a symlink at the destination.\n- Startup claims the pidfile atomically, so two concurrent starts can't leave an\n  orphaned tunnel.\n\n\u003e Account-less quick tunnels have no uptime guarantee, and a freshly provisioned\n\u003e hostname can take a few seconds to tens of seconds for DNS to propagate. The\n\u003e readiness probe waits for that before reporting success; raise `--ready-timeout`\n\u003e on a slow network, or use `--no-verify` to publish immediately and let the\n\u003e consumer retry.\n\n## State\n\nA running instance keeps a small footprint under `$XDG_STATE_HOME/wormhole/`\n(fallback `~/.wormhole/`): `wormhole.pid`, `wormhole.log` (daemon logs), and\n`wormhole.json` (live snapshot used by `status`, `put`, and `url`). All of it is\nremoved on teardown.\n\n## Development\n\n```sh\ngo test ./...     # unit tests + a real lifecycle/cleanup test (uses a fake cloudflared)\ngo vet ./...\ngofmt -l .\n```\n\nTests stub `cloudflared` with a fake binary and set `WORMHOLE_SKIP_PROBE=1`, so they\nneed no network. `WORMHOLE_STATE_DIR` overrides the state directory for isolation.\n\n## Releases\n\nTagging `vX.Y.Z` triggers the release workflow, which cross-compiles binaries for\nlinux/macOS/windows on amd64/arm64, attaches archives + `checksums.txt` to a GitHub\nrelease, and embeds the tag as the build version.\n\n```sh\ngit tag v0.1.0\ngit push origin v0.1.0\n```\n\n## License\n\n[MIT](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebteractive%2Fwormhole","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwebteractive%2Fwormhole","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebteractive%2Fwormhole/lists"}