Skip to main content

SafeEasyAPI (safeeasyapi)

The safe tunnel for API keys. Connecting a website tool usually means 40 minutes of hunting through dashboards, imprecise docs, and fumbling a secret into the right file. safeeasyapi turns it into one command:

safeeasyapi connect github

It opens the exact key page, walks the precise saved steps, takes the key with hidden input, validates it with a test API call, and writes it straight to .env with owner-only permissions — or, with --store keychain, into your OS keychain. The key is never displayed, never logged, never in shell history.

Install

Zero dependencies — Python 3.10+ stdlib only.

pip install git+https://github.com/calasdan-sketch/safeeasyapi
safeeasyapi --version

That installs the safeeasyapi command (py -m safeeasyapi / python -m safeeasyapi works too). From a checkout: pip install .

Windows: run it from PowerShell or Windows Terminal. Under Git Bash / mintty the hidden key prompt (getpass) may echo what you type or appear to hang, because mintty is not a Windows console; if you must use Git Bash, prefix the command with winpty (winpty safeeasyapi connect stripe).

Commands

safeeasyapi list                        # services with saved recipes
safeeasyapi connect <service>           # guided key setup -> .env
safeeasyapi connect <service> --env-file /path/to/.env
safeeasyapi connect <service> --store keychain   # OS keychain instead of .env
safeeasyapi connect <service> --clipboard        # copy-and-go: click Copy, no pasting
safeeasyapi connect github --via-cli    # zero-copy: the vendor CLI's browser sign-in
safeeasyapi connect <service> --no-browser       # don't open the key page (its address is printed)
safeeasyapi run <service> -- <command>  # run with a keychain-stored key in its env
safeeasyapi rotate <service>            # replace a stored key
safeeasyapi forget <service>            # remove the key: OS keychain entry + .env line(s)
safeeasyapi add                         # capture a new service recipe interactively
safeeasyapi find <query>                # find free public APIs for a task
safeeasyapi find <query> --category <name>   # only that category (part of its name)
safeeasyapi find <query> --refresh      # download the directory again now
safeeasyapi mcp                         # stdio MCP server (for Claude Code)

Output can be piped (safeeasyapi list | head -1): when the reader stops early the command ends quietly with exit status 1 — no traceback, no "Exception ignored" noise at exit. On Windows Python reports that closed pipe as a plain OSError 22 instead of BrokenPipeError (a short output only fails when it is flushed); that is recognised too, for every command and for the MCP server.

A connect, annotated

$ safeeasyapi connect stripe

  Connect Stripe  (key opens in your browser)        # 1. your browser opens
                                                     #    the exact key page
    1. If it asks, make sure you are in the right mode: Test keys start
       with sk_test_, live keys with sk_live_
    ...
  Key page: https://dashboard.stripe.com/apikeys

  This key will be validated with: GET https://api.stripe.com/v1/balance
                                                       # 2. EXACT validation
                                                       #    endpoint shown BEFORE
                                                       #    you ever paste the key

  Paste your API key (hidden):                         # 3. hidden paste —
  validating…                                          #    key never displayed
  ✓ added STRIPE_SECRET_KEY in .env (mode 0600 - never displayed, never logged)

(On Windows the line says owner-only ACL instead of mode 0600 — see Where the key goes.)

--no-browser (for connect and rotate) does not open the key page in your browser. Its address is still printed under Key page:, so you can open it yourself: on another machine, over SSH, in a container.

The validation preview is the security hinge: you see exactly where the key will be sent, and the recipe must use https:// for both the key page and the validation URL (a plain-http:// recipe is rejected). If the validation URL belongs to a different owner than the key page, safeeasyapi stops and requires you to type yes — anything else aborts, and no key is ever asked for or sent:

  This key will be validated with: GET https://third-party-validator.example/check
  ⚠ The validation URL is on a DIFFERENT domain than the key page above (third-party-validator.example vs acme.test).
    Your key will be sent to both domains. Make sure you trust both.
  Type 'yes' to continue:                            # Enter alone aborts

"Different owner" means a different registrable domain, not a different hostname: dashboard.stripe.com and api.stripe.com are both stripe.com, so Stripe, GitHub (github.com / api.github.com) and Resend connect without a prompt — the transcript above is the whole flow. Multi-part public suffixes are understood (a.co.uk and b.co.uk are two owners; so are a.github.io and b.github.io, a.blogspot.com and b.blogspot.com, a.run.app and evil.run.app). Case, a trailing dot, a port and user@ in front of the host never change the answer, and names with non-English letters are compared in their xn-- (punycode) form, so bücher.example and xn--bcher-kva.example are one owner. When the domains differ and either one is such a name, the warning shows both spellings (аpple.com [xn--pple-43d.com] vs apple.com, where the first а is Cyrillic) so a look-alike letter cannot hide. A few vendors genuinely keep their API on a second domain (Anthropic's console is platform.claude.com, its API api.anthropic.com; Mailgun .com / .net; Google AI Studio / googleapis.com; Notion .so / .com): their built-in recipes name that host in validate.trusted_hosts and connect without a prompt, with a one-line note. Only recipes shipped inside the package get that shortcut — a recipe from ~/.safeeasyapi/recipes (yours, or one somebody sent you) always prompts when the domains differ, whatever it claims.

Which names count as "anyone can register a sub-domain here" is a compact snapshot, not the full Public Suffix List: country registries by one rule (co, com, org, net, gov, edu, ac, or, ne, go, gob, nom, mil, sch, ltd or plc under a two-letter country code, as in co.uk, com.au, com.gr, gob.mx) plus a few named ones; a list of the hosting and site-builder platforms that hand a sub-domain to anyone (github.io, vercel.app, pages.dev, blogspot.com, myshopify.com, ...); and a short list of clouds where every single host belongs to one customer (*.amazonaws.com, *.run.app). A registry or platform on none of them - the many other country registries (info.pl, gc.ca), dynamic-DNS and small hosting services - is treated as an ordinary domain: two sub-domains there look like one owner and connect does not ask. The validation preview above, which always shows the full URL, is the check that does not depend on the lists.

A recipe is somebody else's text, so it cannot drive your terminal. One that holds a control or invisible character anywhere (an ESC that would hide the lines below it, a newline that forges one, a right-to-left override, a zero-width space) is refused when it is loaded - the other recipes still load, and a one-line warning on stderr names the file and the field - and whatever of a recipe's words reaches the screen is printed so that such a character is shown as a visible escape (\x1b[8m) instead of being sent. Accented and CJK letters and emoji are ordinary text and pass untouched. So does what writing needs in the recipe's own sentences (its name, its steps and its scopes): a space of another width - the no-break space of French typography (Clé API : copiez), the narrow no-break space, the ideographic space - and the zero-width joiner or non-joiner between two visible characters, which glue an emoji such as the woman technologist together and shape Persian and Indic words.

What is refused, exactly: everything Python's str.isprintable() refuses, that is the control characters (ESC, NUL, tab, carriage return, newline and the C1 range), the line and paragraph separators (U+2028, U+2029), the invisible "format" characters (the bidirectional marks and overrides U+200E, U+200F, U+202A to U+202E, U+2066 to U+2069 and U+061C, the zero-width space, the word joiner, the byte order mark, the soft hyphen and tag characters), lone surrogates, private-use code points and code points that are unassigned - except the spaces and joiners above, and those only in a name, a step or a scope. In a URL, a host name, a header or any other field a space of another width or a joiner is refused too, and so is a joiner at the start or the end of a text, or next to a blank or another joiner. An emoji built with tag characters (the flag of England) is refused. What is unassigned is up to the Unicode tables of the Python that runs safeeasyapi: Python 3.10 knows Unicode 13, so an emoji from a later version passes on Python 3.13 and not on 3.10.

In the transcript above the key is shown as sk-… — in reality it is never printed at all, not even redacted.

If the validation endpoint redirects somewhere else — another host, another port, or from https:// down to http:// — the credential header is stripped before the redirect is followed (what browsers and requests do) and the connect stops with validation endpoint redirected to a different origin instead of guessing from a response that proves nothing about the key. A redirect within the same origin (/me → /me/) is followed normally.

A key the request cannot carry is turned back too, before any request is made. An HTTP header holds Latin-1 text only, so a key with a character outside it (a curly quote copied along with the key, an emoji) or with one you cannot see (a line break, a tab) is refused right after the paste, with ✗ that key contains ... Nothing was sent, and you can paste it again. A recipe that uses HTTP Basic puts the key through base64 first, so any character you can see passes there. The message never repeats the key, and neither does the one for anything else the HTTP library might refuse to send.

Run it on your own computer. The check only means something when it is sent straight from your machine. Cloud coding agents, sandboxes and some company proxies add their own credentials to outgoing requests, so the API can answer "valid" for any key, even a made-up one. If a key you know is wrong gets accepted, that is the likely reason: run safeeasyapi connect in a terminal on your own computer instead.

Where the key goes

.env file (default). ENV_VAR=<key> is written to .env (or --env-file; a missing folder is created), private from the first byte: the content goes into a randomly named temp file created with O_CREAT|O_EXCL and mode 0600 in the same directory, is fsync'd, and is then atomically renamed over the target. No reader ever sees a half-written or world-readable key, a crash leaves the old file untouched, and a file somebody pre-planted at a guessable name is never written through. An existing ENV_VAR= or export ENV_VAR= line is replaced in place (keeping export). A value made only of A-Z a-z 0-9 _ . + / = : - is written as it is; any other value is single-quoted (ACME_API_KEY='ab#c d"e'). A shell that sources the file takes every character inside single quotes literally, so $(...), backticks and $VAR in a value can never run or expand (inside double quotes they do), and an unquoted # or space cannot cut the value short. A key with a single quote, a backslash, ${ or a character you cannot see (a tab, a line break, a zero-width or no-break space) is refused: the readers disagree about those, real API keys contain none of them, and a pasted key that has one is nearly always a copy accident (a quote mark copied along with the key, say). connect says so right after the paste, before the key is sent anywhere, and lets you paste it again (the OS keychain, --store keychain, does not apply this rule). If the file cannot be written (--env-file in a folder that is a file, or unwritable), the connect ends with ✗ the key was validated but could NOT be saved and exit 1 — the key is never printed, and nothing is written.

What was checked. The tests source the file in a real sh and bash (skipped where there is no POSIX shell) and split every line with Python's shlex: none of the awkward values ($(...), backticks, ;, &, $HOME, ~, spaces, #, quote marks) runs anything or comes back different. The other readers were checked by hand on Linux in October 2026 with about sixty awkward values, written by the real code: Node dotenv 17.4.2, python-dotenv 1.2.3 and docker compose 5.5.1 all gave every storable value back exactly. That is why ${ and a backslash are refused rather than written: python-dotenv expands ${NAME} and reads \\ as one backslash even inside single quotes, and docker compose rejects a value that ends in a backslash. Tools that expand $ after reading the file (frameworks that add dotenv-expand, for example) were not checked and may treat a $ in a value specially.

Older files. Versions 0.3.1 and earlier wrote double quotes, inside which a shell still runs $(...) and backticks and expands $VAR. A key made only of letters, digits and _ . + / = : - was never quoted, so those are unaffected. Double-quoted values are still read, and such a line is rewritten in the new form the next time its key is saved: safeeasyapi rotate <service> does it, and nothing else in the file moves.

If the same variable is set more than once in the file, the last line is the one that is updated: python-dotenv, Node dotenv, docker compose and a shell source all let the last assignment win, so changing the first would save a key that never takes effect. Earlier lines are left exactly as they were, and the success line says so (ACME_API_KEY appears twice in .env; I updated the one that takes effect).

A quoted value that runs over several lines (a PEM block in single quotes, say) belongs to its variable, the way python-dotenv, Node dotenv and a shell read it: a line inside it that happens to look like ACME_API_KEY=... is part of that value, so it is never rewritten or removed, and if the variable's own value runs over several lines, they are replaced (or removed by forget) together. A quote that is never closed, or whose closing line has other text after it, is read as a one-line value, as those readers do.

Only your key's line changes; every other byte of an existing file is kept exactly as it was. A file saved in a Windows code page (cp1252: an accented comment from Notepad) is not valid UTF-8, and a key is still saved into it without complaint. A CRLF file stays CRLF and an LF file stays LF: the line that is added or changed uses the file's own style, a missing final newline is added in that style, and a brand-new file uses LF. The byte-order mark that PowerShell and some Windows editors put at the start of a UTF-8 file is kept, and does not hide a key on the first line. Two things are refused with one plain sentence and exit 1, before the browser opens or any key is asked for: a .env that is a link to another file (a key is never written through a link, and a link is never silently replaced — name the real file with --env-file if you mean it), and a file that is not plain text, such as the UTF-16 that Windows PowerShell's > creates (appending UTF-8 to it would wreck it — re-save it as UTF-8).

  • Linux / macOS: the file is chmod 0600 (owner read/write only).
  • Windows: POSIX mode bits don't exist, so the file's ACL is reduced to the current user (icacls <file> /inheritance:r /grant:r <user>:(F), best effort). The success line says owner-only ACL only when icacls actually succeeded; otherwise it says could not restrict ACL so you know the file has the folder's default permissions.

safeeasyapi forget <service> undoes a connect: it deletes the keychain entry (when this OS has a keychain) and removes the ENV_VAR= line from .env (--env-file to point elsewhere), and says what it removed. If the variable is set more than once, every one of those lines goes — otherwise an older one would take over again — and it says how many.

On Linux, secret-tool clear exits 0 whether or not it found anything, so forget looks the entry up first and clears it only when it is there. Run twice, the second forget says nothing stored instead of claiming to have removed the entry again; an entry that is there and cannot be cleared is reported as such, with exit status 1.

forget also looks for the temp file an interrupted write (a kill -9, a power cut) can leave next to the .env file - ..env. and eight random characters and .tmp, a complete copy of the file - and deletes it when it still holds the key; the success line names it. Other names of the file are another matter: a hard link (a .env.bak made with ln) keeps the old contents, key included, however the file is rewritten. forget cannot change that and does not delete files it did not make: it names the hard link, says the key is still there, and exits 1 instead of claiming it is gone. Copies it cannot know about - an editor's backup (.env~), version-control history, a shell history, a backup of the folder - are yours to remove.

OS keychain (--store keychain). No keyring package; each OS uses what it ships with, and the key never goes through argv:

OS backend how
macOS login Keychain, via the security command commands fed over stdin (security -i), generic password safeeasyapi:<service> / account <ENV_VAR>
Windows Credential Manager (Windows Vault), via ctypes → advapi32 CredWriteW/CredReadW/CredDeleteW generic credential safeeasyapi:<service>, persisted for the local machine, UTF-16-LE blob like cmdkey
Linux Secret Service (GNOME Keyring / KWallet) via secret-tool, when installed key on stdin; attributes service=safeeasyapi account=<ENV_VAR> slug=<service>
anything else, or the tool is missing — falls back to the .env file and says so

A keychain-stored key is consumed with safeeasyapi run <service> -- <command>: the key is read back and placed in that one child process's environment, never printed. (Your app does not need to change — it still reads $STRIPE_SECRET_KEY.) Without a keychain backend run exits 1 and tells you the key lives in .env.

run hands back the command's own exit status. A command that cannot be started is reported in one line, with no traceback and before the keychain is asked for the key: exit status 127 for a command that is not on your PATH (command not found), 126 for one that is there but cannot be run (not an executable file, a directory). A command that a signal stops gives 128 plus the signal number (137 for a kill), as in a shell. Ctrl-C ends run, the MCP server or any other command with interrupted. and exit status 130. On Windows the command is looked up the way the shell does it (PATHEXT), so npm finds npm.cmd.

A keychain tool that does not answer within 30 seconds, or cannot be started, is handled like any other keychain failure. connect --store keychain falls back to the .env file and says so, so a key that was just validated is never lost; forget still removes the .env line(s), then says the keychain entry may still be there and exits 1 (run it again when the keychain answers); run exits 1 with a message.

forget says what it removed before it says what it could not do. With a .env that is a link (never edited, see Security model) it still removes the keychain entry and any leftover copies of an interrupted write, then names the link and exits 1: the key may still be in the file the link points to.

Find public APIs

SafeEasyAPI is also a discovery front door. safeeasyapi find searches the public APIs directory (~2,100 free APIs, cached locally for 7 days):

safeeasyapi find "weather" --auth No        # only keyless APIs
safeeasyapi find "crypto price" --limit 5
safeeasyapi find "facts" --category animals # only that category
safeeasyapi find "weather" --refresh        # download the directory again now

--auth takes the directory's own word (No, apiKey, OAuth, ...) and --limit caps the list (10 by default). --category keeps one category: a part of its name, in any case (animals finds Animals). --refresh downloads the directory again now, whatever the age of the copy.

A copy older than 7 days is downloaded again, and so is one dated in the future by more than an hour (a clock that was wrong when it was saved): without that, the old list would be served until that date. A copy dated a few seconds or minutes ahead is normal - two clocks never agree exactly - and is used. The file is swapped in whole, so an interrupted download never leaves half a list behind.

APIs needing a key point straight back at the tunnel:

    Weatherstack [apiKey]
      Real-time weather
      https://weatherstack.com/  (Weather)
      needs a key — save its recipe with `safeeasyapi add`.

MCP co-work skills

The MCP server exposes the directory to Claude Code as tools, so the agent discovers free APIs instead of improvising or reaching for paid services:

  • find_public_apis {query, auth?, category?, limit?} — search the directory
  • get_public_api {name} — details plus wiring: no-auth APIs get a call-it-directly note; apiKey APIs get the exact safeeasyapi add + safeeasyapi connect flow (the agent never asks the user to paste a key)

No secrets ever pass through MCP — only public how-to.

Copy-and-go, two-value keys, every kind of check (0.4.0)

Copy-and-go (--clipboard): instead of pasting, click the vendor's own Copy button. safeeasyapi notices the new, key-shaped value on the clipboard, takes it and empties the clipboard, then validates and stores it as usual; nothing is typed, pasted or shown. It looks only while it is waiting for a key, takes only a value that matches the field's pattern (when the recipe has one) or looks like a key (one line, no spaces, 8-4096 visible characters, different from what was already on the clipboard), and falls back to the hidden prompt after 3 minutes or when no clipboard tool exists (Windows needs nothing; macOS uses pbpaste; Linux wl-paste, xclip or xsel). Windows clipboard history (Win+V) and clipboard managers can keep their own copy, which safeeasyapi cannot clear.

Zero-copy (--via-cli): for a built-in recipe whose vendor has a CLI with a browser sign-in (github uses gh auth login --web), safeeasyapi runs that sign-in and reads the token from the CLI, then validates and stores it. The programs a recipe may run are fixed in the code (recipes.CLI_PROGRAMS); a recipe from ~/.safeeasyapi cannot use this.

Two-value credentials: a recipe may list several fields (an App ID and a secret, an access key and its secret). Each is collected hidden (or copied), they are validated together, and each is stored under its own variable; run injects them all and forget removes them all.

Every common check: besides bearer, header and basic, a recipe can use headers (several headers from {field} templates), basic with a field as the username (username_field), query (the key as a URL parameter; such a request follows no redirect), oauth2_client_credentials (the client id and secret go in ONE POST to the vendor's token_url, which follows no redirect, is shown in the preview, must be https:// and needs a typed yes on another domain; the short-lived token then makes the GET, or with token_only the token answer is the verdict), and aws_sigv4 (a signed STS GetCallerIdentity; the secret signs and is never sent, and the signature is tested against botocore's). A recipe is refused at load time when it names a field it does not collect, collects one it never checks, or puts its token endpoint on http://.

Why it exists

Agents (and humans) forget per-service key flows and improvise imprecise steps. Recipes are the memory: plain JSON files with the key URL, the exact click path, scopes, and a validation endpoint. Teach it once with safeeasyapi add, use it forever — and Claude Code can pull the exact recipe via safeeasyapi mcp instead of guessing.

Ships with 35 recipes inside the package (so pip install carries them), each checked against the provider's own docs for the key page, the validation endpoint and the auth header, and smoke-tested live with a fake key (every one answers with a clean "key rejected"):

area services
LLM / AI openai, anthropic, google-gemini, groq, mistral, cohere, huggingface, together, openrouter, replicate
speech elevenlabs, deepgram, assemblyai
infra github, supabase (Management API token), vercel, netlify, cloudflare, pinecone
email / payments stripe, resend, sendgrid, mailgun, postmark (server token)
productivity / search notion, airtable, discord (bot token), tavily, brave-search
two values / other checks (0.4.0) ebay and spotify (OAuth2 client credentials), paypal (OAuth2, token only), twilio (Account SID + token), aws (SigV4), openweather (key in the URL)

Each validation call is a free, read-only request (list models, "who am I", verify token) — except brave-search, whose only authenticated endpoint is the search itself, so validating costs one tiny query (the recipe says so). The 0.4.0 recipes were written from each provider's docs, checked against a local fake API, and smoke-tested live with fake credentials (each answers "rejected"). Not included: Slack (auth.test answers HTTP 200 even to a bad token), Linear (GraphQL needs a POST body), Shopify (per-store URL), Perplexity/Exa/Serper (no free GET endpoint), Fireworks (the documented list-models URL is per account).

safeeasyapi add walks you through capturing your own (Headroom, RPX, Ponytail…); those are saved to ~/.safeeasyapi/recipes/ (%USERPROFILE%\.safeeasyapi\recipes\ on Windows) and override a built-in recipe with the same slug. --recipes-dir writes elsewhere. A recipe file may be saved with Windows (CRLF) line ends and a UTF-8 byte-order mark, as Windows PowerShell and older Notepad write them: a recipe saved with one loads like any other. A slug is [a-z0-9][a-z0-9_-]{0,63} — it becomes the file name, so ../../evil is refused, and so is a name Windows keeps for a device (con, prn, aux, nul, com0 to com9, lpt0 to lpt9: a file called nul.json cannot be saved there, and the refusal says so in one line) — and the expected status codes must be numbers (200,201). The validation call must be a GET or HEAD: it only reads, so a recipe cannot make connect send your key with a DELETE or a POST. Besides bearer / header / basic, a recipe's validate block may set auth.scheme (Token for Deepgram, Bot for Discord), auth.username for Basic auth with the key as the password (Mailgun's api:<key>), static headers (anthropic-version, Notion-Version), and reject — the statuses that mean "bad key" when a provider does not answer 401/403 (Google and Resend say 400, Brave 422).

The MCP server

Add to Claude Code (claude mcp add) or ~/.claude.json:

{ "mcpServers": { "safeeasyapi": {
  "command": "python", "args": ["-m", "safeeasyapi", "mcp"] } } }

(python is the Windows launcher's name; on macOS/Linux use whatever runs your Python 3.) Tools: list_services, get_recipe, find_public_apis, get_public_api. The recipe tells the agent the exact steps and ends with: run safeeasyapi connect <service> in a terminal — never ask the user to paste the key into chat. The server speaks JSON-RPC 2.0 properly: notifications (notifications/cancelled, notifications/initialized, anything without an id) are never answered, a non-object message gets -32600 Invalid Request, bad JSON (a hundred thousand open brackets included) -32700 Parse error, and params that is not an object (null, a list, a string…), arguments that is not an object, or a missing or wrongly typed required argument gets -32602 Invalid params — never a crash or -32603 — after which the server simply carries on. (A tool-level miss, such as a service that has no recipe, is an ordinary result with isError.) If standard input or standard output is closed (safeeasyapi mcp >&-; pythonw on Windows) there is no client to talk to: one error: line on stderr and exit status 2, before any request is read, not a traceback.

Security model

  • Key input via getpass (no echo); never printed, never logged.
  • .env written atomically through a random, exclusive, 0600 temp file + rename (owner-only ACL on Windows); keychain storage never passes the key on a command line.
  • A .env that is a link is refused, never written through or replaced, and the permission step (chmod / icacls) never follows a link to change something else.
  • Whatever a recipe or a paste contains, the .env file gets exactly one NAME=value line from it: a name is a plain identifier (a trailing newline does not slip through), and a value is single-quoted or refused, so sourcing the file runs nothing.
  • Validation is a single minimal HTTPS call; only the status is read, response bodies are discarded. The credential never follows a redirect to another origin or to plain http://.
  • Rejected keys are scrubbed immediately; 3 failed attempts aborts.
  • The exact validation URL and HTTP method are printed BEFORE the key is asked for; a validation URL under another registrable domain needs a typed yes (built-in recipes may declare their vendor's second API domain; user recipes cannot).
  • A reply that is not HTTP at all (a captive portal, a TLS mismatch, a non-HTTP service on the port) is reported as a validation error, not a traceback.
  • A recipe with a control or invisible character in it is refused at load time, and any recipe text that reaches the screen is shown as a visible escape, so a recipe cannot hide or forge the validation preview. (The one leeway: a space of another width and a joiner between two visible characters are text in a name, a step or a scope - the list of what is refused is in A connect, annotated.)
  • The only POST safeeasyapi sends is the OAuth2 token request, to the token_url the recipe names and shown before anything is asked for; it and a query request follow no redirect. Copy-and-go empties the clipboard as soon as it takes a value.
  • Recipes must use https:// for the key page and the validation URL — plain-http:// recipes (and ones whose URL is malformed) are rejected at load time.
  • Recipes contain no secrets — only public how-to.

Testing

python -m unittest discover -s tests -v
python -m pytest -q                    # same suite

The tests use only the standard library and no network beyond 127.0.0.1 (nothing is downloaded, not even to build a wheel). Two groups skip, with a reason, where they cannot run: the tests that build the wheel and the sdist need a setuptools 77 or newer, with a packaging 24.2 or newer, installed in the Python that runs them; the tests that read pyproject.toml need tomllib, so they skip on Python 3.10. What they cover: recipe schema, HTTPS-URL rejection and malformed URLs, slug validation, every shipped recipe (parses, https, file name = slug, credential header built), .env create/update/permissions (random exclusive temp file, crash safety, never world-readable, value quoting (sourced in a real shell, split with shlex, values the readers disagree on refused), export lines, an unwritable path, a cp1252 file, a byte-order mark, CRLF/LF/mixed endings and a variable set twice (the last line wins) kept byte-for-byte, a UTF-16 file and a symbolic link refused, permissions that never follow a link), redirect credential stripping against a local redirecting server pair, full connect flow against a local fake HTTP service (bearer/header/basic auth, custom schemes and static headers, 401 rejection, custom reject statuses, network failure, a non-HTTP reply), validation-URL preview ordering, registrable-domain matching (punycode names, trailing dots, ports, user@, a look-alike letter shown in the warning) and typed-yes on a real mismatch, keychain dispatch for every OS with the real keychain faked (the Windows path through a fake advapi32), the .env fallback, forget, the honest Windows ACL line, MCP provenance labels and quarantine of poisoned directory entries, JSON-RPC notification, invalid-request and invalid-params handling, MCP protocol round-trip over real stdio, a wheel build that must contain the recipes and an sdist that must carry the tests (both built from a clean copy of the folder, so a leftover build/ cannot reach them), the package metadata and one version number everywhere, output into a pipe that closes early (the real thing, and the different error Windows gives, simulated), and never-log assertions on every output path. Tests point HOME/USERPROFILE/APPDATA at a throwaway directory, so they never read your real ~/.safeeasyapi or .env.

Windows 11 is a first-class target: the wire format of the MCP server is UTF-8 bytes regardless of console code page, console output is reconfigured to replace unencodable characters instead of crashing, and every text file is opened as UTF-8.

Scope / limits (v0.4.1)

  • Keychain storage is best effort and opt-in (--store keychain); the .env file is the default and the fallback. No Doppler / Vault backends.
  • Recipe catalog is curated manually — verify a new service's key URL and validation endpoint before trusting it. Provider dashboards move: if a key page URL redirects, follow it and send a fix.
  • safeeasyapi mcp returns recipes; the interactive key paste stays in the terminal by design.
  • forget removes what safeeasyapi wrote and what a killed write left next to it; it does not find other copies of the file (editor backups, version-control history), and it names - rather than deletes - another hard link to the .env file, exiting 1 because the key is still there.
  • Windows: run finds npm.cmd and other .cmd/.bat commands through PATHEXT. That lookup is tested with a stand-in for the Windows search, not on a Windows machine, and cmd.exe, which starts such a file, reads & | < > ^ % in its arguments as its own syntax: do not pass one text you do not trust.
  • "Same owner" (the cross-domain prompt) knows a compact snapshot of country registries and hosting platforms, not the whole Public Suffix List: a platform it does not list is treated as an ordinary domain, so two tenants of it do not trigger the prompt. The validation preview always shows the full URL.
  • Linux: secret-tool is driven through stand-ins in the tests, not against a real Secret Service (GNOME Keyring, KWallet): none was available. That includes forget, which relies on secret-tool lookup finding nothing (exit status 1) when no entry matches.
  • Windows: the refusal of device names (nul, con, com1 ...) as recipe slugs is a plain list of names, tested on Linux, not on a Windows machine.
  • Windows: the owner-only ACL goes on the .env file right after it is moved into place, not before, so for that moment it has the ACL its folder gives it. This is read from the code and checked against a stand-in for icacls, not on a Windows machine.
  • The repository's own text files are committed with Windows (CRLF) line endings and there is no .gitattributes: keep them when you edit a file. On Linux and macOS git diff flags them as trailing whitespace.

Metadata

Release files for safeeasyapi 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for safeeasyapi 0.4.1
File Size Uploaded Provenance
safeeasyapi-0.4.1.tar.gz 197.8 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for safeeasyapi 0.4.1
File Interpreter ABI Platform
safeeasyapi-0.4.1-py3-none-any.whl Python 3 none any Details

Total release size: 295.9 kB

Release files / safeeasyapi-0.4.1.tar.gz

Download URL safeeasyapi-0.4.1.tar.gz
Size 197.8 kB
Tags Source
SHA-256 checksum
How to use checksums
c936dcf43fce537a6eabec71523a9858ec3703766b7ee7a5bf74fa21218aac47
BLAKE2b-256 checksum
How to use checksums
59a32aa1ccb433f137e60c83e7065c4af9722f83decb6d44f228376496b021ad
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / safeeasyapi-0.4.1-py3-none-any.whl

Download URL safeeasyapi-0.4.1-py3-none-any.whl
Size 98.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5849e2ab085e4ab201fee1f11b5a4da572782a46051b929291a788c32404ff89
BLAKE2b-256 checksum
How to use checksums
565978a8950c305201908e8a4abfd8963197ca6bb3c55f051ae72f71c4ca0d5a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Security for safeeasyapi 0.4.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page