airing
by Blackman99
README.md
<p align="center"><img src="assets/logo-512.png" width="128" height="128" alt="airing logo: scarlet briefs with a keyhole, pegged on a washing line"></p>
<h1 align="center">airing</h1>
<p align="center"><b>See what your deploy hangs out in public — before your agent runs it, and after it's live.</b><br>
<a href="https://blackman99.github.io/airing/">site + live demo</a> · <a href="https://blackman99.github.io/airing/docs/">docs</a> · <a href="GOAL.md">goal</a></p>
<p align="center"><img src="assets/demo.gif" width="860" alt="The airing laundry line: files from a sample project are pegged out one by one; the .env comes out scarlet with a tag reading PUBLIC · GOOGLE_API_KEY +3. Switching host to Netlify reels the line in and re-hangs it with the .env kept in the basket."></p>
## The problem
`wrangler pages deploy .` uploads your `.env`. So does Firebase (dotfiles are in), and Workers assets ship even `.git/`. Vercel skips `.env.local` but not `.env` or `.env.production`. **None of them read your `.gitignore`.** A coding agent that deploys "the folder that works" can publish your keys — [one leaked Gemini key cost $90k this week](https://x.com/JustLingonberry/status/2110824346168238204).
airing reads the folder the way each host's CLI actually does (rules ported from their source and cross-checked against the real libraries in CI) and tells you which secrets would sit at a public URL, plus the one change that keeps them home. Names, sizes and SHA-256 fingerprints only — it never prints a value.
## Try it (10 seconds, nothing to install)
```bash
cd my-site
npx github:Blackman99/airing .
```
```console
$ npx github:Blackman99/airing .
airing · Cloudflare Pages · ./ (package.json scripts.deploy)
15 files go public · 7 stay home
LEAK .env 314 B env file — 5 variables, 4 look secret
GOOGLE_API_KEY Google Cloud / Gemini AIza…(39 chars) sha256:1aa0cc79
rotate → https://console.cloud.google.com/apis/credentials
OPENAI_API_KEY OpenAI sk-proj-…(72 chars) sha256:41d45bff
…
LEAK backup.sql 111 B database dump — your data, downloadable in one request
RISK dist/assets/index-8f3a.js.map 135 B source map with original source
…
keep it home: you are deploying the project root — deploy the build output instead: wrangler pages deploy dist
saved by the host: .git/ (wrangler IGNORE_LIST "**/.git")
✗ 2 leaks would be public. nothing was uploaded — airing only reads. Fix, then deploy.
```
Exit code `1` means something would leak, `0` means clean, `2` means a usage or I/O error — so it drops straight into scripts.
## Stop the agent before it deploys
**Claude Code** — a `PreToolUse` hook that denies a leaking `wrangler` / `vercel` / `netlify` / `firebase` / `gh-pages` command and hands the agent the fix:
```bash
npm i -g https://github.com/Blackman99/airing/releases/latest/download/airing.tgz # once, so the hook starts in ms
```
```jsonc
// .claude/settings.json
{ "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "airing hook" }] }] } }
```
**Any agent or human** — put `guard --` in front of the deploy; it runs only if nothing would leak:
```bash
airing guard -- npx wrangler pages deploy dist
```
## Check what is already public
```bash
airing probe https://my-site.pages.dev
airing probe https://blackman99.github.io/airing/leaky-demo/ # our deliberately leaky page
```
~40 well-known paths (`/.env*`, `/.git/config`, keys, dumps, agent settings) plus the source maps of the page's own scripts, soft-404 aware so an SPA catch-all never counts. Only probe sites you own; if it finds a key, rotate it first.
## In CI, and over MCP
```yaml
# after your build step
- uses: Blackman99/airing@v0
with:
path: .
probe-url: https://my-site.pages.dev # optional, after deploy
```
Outputs `leaks`, `risks`, `json`; findings are annotated on the files and written to the job summary. `fail-on: leak | risk | never`.
```json
{ "mcpServers": { "airing": { "command": "npx", "args": ["-y", "github:Blackman99/airing", "mcp"] } } }
```
Read-only tools: `scan_deploy`, `probe_url`, `host_rules`.
## What each host publishes by default
| host | `.env` | `.git/` | `*.map` | reads `.gitignore`? | how to keep files home |
|---|---|---|---|---|---|
| Cloudflare Pages (`wrangler pages deploy`) | **public** | home | **public** | no | deploy the build dir — no ignore file for direct uploads |
| Cloudflare Workers assets | **public** | **public** | **public** | no | `.assetsignore` |
| Vercel (static) | **public** | home | **public** | no | `.vercelignore` |
| Netlify | home | home | **public** | no | keep them out of the publish dir |
| Firebase Hosting | **public** | **public** | **public** | no | `hosting.ignore` in firebase.json |
| GitHub Pages (Actions) | home | home | **public** | no | don't set `include-hidden-files: true` |
| gh-pages (npm) | home | home | **public** | no | don't pass `--dotfiles` |
`airing hosts` prints these rules with links to the upstream source each one is ported from.
## Install
airing is **not on the npm registry yet** (so `npx airing` won't find it). Run it from GitHub, or install the release tarball — it has no runtime dependencies and `dist/` is committed, so nothing builds on install. (Use the tarball for a global install: npm 11's `npm i -g github:…` links a temp clone and leaves a dead `airing` command.)
```bash
npx github:Blackman99/airing . # one-off, nothing installed
npm i -g https://github.com/Blackman99/airing/releases/latest/download/airing.tgz # the `airing` command (latest release)
npm i -g https://github.com/Blackman99/airing/releases/download/v0.1.0/airing-0.1.0.tgz # pinned
```
Node 18.17+. Full reference, configuration and FAQ: [docs](https://blackman99.github.io/airing/docs/).
MIT · [goal](GOAL.md) · [contributing](CONTRIBUTING.md) · [roadmap](ROADMAP.md)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues