DEV Community

Cover image for Build a disposable email alias CLI in 50 lines of Bash
anon.li
anon.li

Posted on

Build a disposable email alias CLI in 50 lines of Bash

I have a rule I keep breaking: never give a website my real email if I'm not sure I want a relationship with them. Five minutes later I'm on some forum, signup form open, and I'm typing me@gmail.com again because firing up a password manager, generating an alias, copying it, pasting it - it's just enough friction that I default to the lazy thing.

So this weekend I built alias - a single shell command that spits out a fresh forwarding address I can paste anywhere. Took about an hour. The whole thing is curl, jq, and one POST request.

I'm using anon.li as the backend because (a) it has a clean REST API, (b) it's open source, and (c) the free tier covers personal use. But the structure here works against any forwarding service that exposes an API - Addy, SimpleLogin, your own self-hosted thing.

The endpoint

The Alias API has a Bitwarden-compatible "just generate me one" endpoint. From the docs:

POST https://anon.li/api/v1/alias?generate=true
Authorization: Bearer ak_...
Enter fullscreen mode Exit fullscreen mode

You can pass an optional domain in the body if you've added a custom one. With no body, it returns a random alias on anon.li. The response is the canonical alias object - id, email, timestamps, the works.

That's literally the entire API surface for what I want. One call, one address.

Step 1: get a key

Sign up, go to Dashboard → API Keys, generate one. It'll look like ak_ followed by 32 hex characters. Copy it once, because anon.li only shows the SHA-256 hash on their side after that - if you lose it, you rotate.

I stash mine in ~/.config/anon/key:

mkdir -p ~/.config/anon
echo "ak_yourkeyhere" > ~/.config/anon/key
chmod 600 ~/.config/anon/key
Enter fullscreen mode Exit fullscreen mode

If you're on a multi-user machine or just paranoid, swap this for pass, 1Password CLI, gpg --decrypt, whatever. Don't put it in .bashrc in plaintext.

Step 2: the function

Drop this into ~/.bashrc (or ~/.zshrc):

alias() {
  local key
  key=$(cat ~/.config/anon/key 2>/dev/null) || {
    echo "no api key at ~/.config/anon/key" >&2
    return 1
  }

  local domain="${1:-anon.li}"
  local response
  response=$(curl -sS -X POST \
    "https://anon.li/api/v1/alias?generate=true" \
    -H "Authorization: Bearer $key" \
    -H "Content-Type: application/json" \
    -d "{\"domain\":\"$domain\"}")

  local email
  email=$(echo "$response" | jq -r '.data.email // empty')

  if [[ -z "$email" ]]; then
    echo "alias creation failed:" >&2
    echo "$response" >&2
    return 1
  fi

  echo -n "$email" | (command -v pbcopy >/dev/null && pbcopy \
                  || command -v wl-copy >/dev/null && wl-copy \
                  || command -v xclip >/dev/null && xclip -selection clipboard)
  echo "$email (copied)"
}
Enter fullscreen mode Exit fullscreen mode

Reload your shell. Now:

$ alias
x7k9m2@anon.li (copied)
Enter fullscreen mode Exit fullscreen mode

That's it. Forty-something lines of Bash, one network call, address on my clipboard. I haven't typed gmail.com into a signup form in six weeks.

Heads up: alias is a Bash builtin (the thing you use for alias ll='ls -la'). Defining a function with the same name shadows it inside your own shell, which is fine for personal use, but if you script anything that depends on alias the builtin, name yours aka or alli or whatever. I personally don't care.

Why not just use the dashboard?

I do, for ones I want to label and track. But for a throwaway - "I just want to read this gated article" - I don't need to think about what to call it. I want it on my clipboard before I've finished alt-tabbing back to the browser. The CLI wins on that exact use case.

The dashboard is also where I go later to disable the alias when the site inevitably starts spamming me. Toggling active: false is one PATCH request, but I rarely bother scripting that part - I'm already in the dashboard reading the spam.

Bonus: domain switcher

If you've added a custom domain (say, mail.example.dev), you can pass it as an argument:

$ alias mail.example.dev
random-suffix@mail.example.dev (copied)
Enter fullscreen mode Exit fullscreen mode

I use this for my "real but disposable" identity - the one I give to recruiters and conference signups. Random-looking but on a domain I own, so it doesn't trip the "is this a burner?" filters that some sites run against known alias domains.

Bonus 2: skip the CLI, use Bitwarden

If you live in Bitwarden anyway, anon.li implements the Addy.io-compatible flow. In Settings → Options → Username Generator, pick Forwarded Email Alias → addy.io, paste your API key, and set the URL to https://anon.li/api/v1. Now the username field on every "create new login" sheet has a generate button that talks to the same endpoint. That's the fully no-friction version.

The CLI still wins for terminal flows - quick test signups, registering API sandboxes, anywhere I'm not in the password manager UI.

What's actually happening behind the curtain

The Alias backend isn't a hosted mailbox. There's no inbox. anon.li's mail server takes the inbound message, optionally encrypts the forwarded copy with your recipient's PGP key, signs it with DKIM, rewrites the envelope sender (SRS) so SPF still aligns, and ships it to your real address. The only state stored per alias is aggregate counters: received, blocked, last-seen-at.

That trust model is what makes me comfortable using disposable aliases for anything moderately sensitive - receipts, account confirmations, the dentist. Even if anon.li got fully owned tomorrow, there's no historical archive of my mail to leak. Just the routing config.

It's not a replacement for E2EE messaging. SMTP is what it is. But it raises the floor by a meaningful amount, and the API makes it easy enough that I actually use it instead of giving up and pasting my real address.

The whole script

For copy-paste convenience:

# ~/.bashrc
alias() {
  local key
  key=$(cat ~/.config/anon/key 2>/dev/null) || {
    echo "no api key at ~/.config/anon/key" >&2
    return 1
  }
  local domain="${1:-anon.li}"
  local response
  response=$(curl -sS -X POST \
    "https://anon.li/api/v1/alias?generate=true" \
    -H "Authorization: Bearer $key" \
    -H "Content-Type: application/json" \
    -d "{\"domain\":\"$domain\"}")
  local email
  email=$(echo "$response" | jq -r '.data.email // empty')
  if [[ -z "$email" ]]; then
    echo "alias creation failed:" >&2
    echo "$response" >&2
    return 1
  fi
  echo -n "$email" | (command -v pbcopy >/dev/null && pbcopy \
                  || command -v wl-copy >/dev/null && wl-copy \
                  || command -v xclip >/dev/null && xclip -selection clipboard)
  echo "$email (copied)"
}
Enter fullscreen mode Exit fullscreen mode

Friction is the enemy of good security habits. The smaller you can make the gap between "I want to do the right thing" and "the right thing is done," the more often you'll do it. A single shell function won me back the habit. Try it.

Top comments (0)