Local secret broker that keeps API keys out of files AI agents can read.
Project description
keyward
A local secret broker for developers who run AI coding agents on their own machines.
The goal
Keep API keys out of any file an AI agent, co-pilot, or third-party tool can read, without adding friction to normal development.
Your code never contains real keys. It contains opaque tokens like kw_ab12cd34.
A local daemon swaps the token for the real key only when the outbound request
goes to an allowlisted endpoint, and records every use.
If an agent reads your code, config, or environment, it sees tokens. Tokens are useless off-host: they only resolve inside the daemon, which will not forward them to destinations you have not explicitly approved.
Why this is not just encryption
"One-way encryption you can decrypt" does not exist. What this package actually provides is tokenization plus a scoped, audited forward proxy. The security properties that matter are:
- real secrets live in the OS keychain, never on disk in plaintext
- code and config contain only tokens
- the daemon forwards to an allowlist, so a leaked token cannot exfiltrate data to a new host
- every resolution is logged
- new destinations require explicit user approval
Intended user experience
Onboarding is the product. If any step feels heavier than export KEY=...,
it has failed its design goal.
# one-time setup
pip install keyward
keyward init
# add a key (prompts for the secret; never passed on the command line)
keyward add openai --endpoint api.openai.com
# run any program with tokens injected as env vars
keyward run -- python app.py
keyward run -- pytest
keyward run -- npm start
# rotate a key in place; tokens stay the same so no code changes
keyward rotate openai
# list, remove, inspect
keyward list
keyward rm openai
keyward log --since 1h
Your code stays boring:
import os, openai
client = openai.OpenAI() # reads OPENAI_API_KEY and OPENAI_BASE_URL from env
Under keyward run, those variables point at the local daemon with a token.
Outside keyward run, they are not set at all.
Activating from inside your app
If you don't want to wrap every command with keyward run, call
keyward.activate() once near the top of your app. With the daemon installed
as a login agent (keyward init), this is all you need:
# .env (or your normal env-loading mechanism)
# OPENAI_API_KEY=kw_ab12cd34
import os
from dotenv import load_dotenv
load_dotenv()
import keyward
keyward.activate() # rewrites OPENAI_BASE_URL to point at the daemon
from openai import OpenAI
client = OpenAI() # transparently goes through keyward
activate() looks at every registered key, and for each one whose env_vars
already hold its token in os.environ, sets the matching base_url_env to the
daemon URL. Real keys are left alone. It also exports KEYWARD_DAEMON as a
stable signal you can check from your code (if "KEYWARD_DAEMON" in os.environ: ...) to confirm activation.
It returns a keyward.ActivateResult with three lists so you can see exactly
what happened:
result = keyward.activate(strict=False)
if result.skipped_no_env:
print(f"token not found in env for: {result.skipped_no_env}")
print("Did you load your .env file before calling activate()?")
# result.activated — keys that are now routing through the daemon
# result.skipped_no_env — keys whose token was not found in any env var
# result.skipped_no_base_url — keys with no base_url_env configured
If no daemon is running, activate() raises keyward.DaemonNotRunning. Pass
strict=False to return an empty result instead — useful for code that should
work both with and without keyward installed.
What works today (v0.2)
| Area | Status |
|---|---|
| CLI commands | init, add, list, rm, rotate, restart, run all functional |
| Keychain storage | macOS Keychain, Windows Credential Manager, Linux libsecret via keyring |
| Proxy forwarding | Authorization: Bearer and x-api-key, on both ingress and egress |
| Streaming | Server-Sent Events forwarded without buffering |
| Login agent | macOS LaunchAgent install/uninstall/kickstart via keyward init |
| Daemon reuse | keyward run reuses a live daemon; else spawns ephemeral |
| Audit log | Stub only (prints TODO; no log is written yet) |
| Endpoint enforcement | Each token is bound to one host at keyward add time; the daemon ignores the request host and always forwards to the stored endpoint — so a token cannot be used against a different host |
| Multi-endpoint allowlist + approval flow | Not yet — v0.3 scope; see ARCHITECTURE.md |
| Linux systemd / Windows scheduled task | Not wired up yet |
| Websocket proxying | Returns 501; HTTP only for now |
| Request body streaming | Buffered; fine for LLM chat, not for large uploads |
| Caller attestation | Trust-anything on localhost; see ARCHITECTURE.md |
See docs/ARCHITECTURE.md for the full design, threat model, and the list of deferred items.
Verifying the key swap
The sharpest test is to point keyward at a request-echoing endpoint and look for your raw secret (and the absence of the token) in the response.
# pick a distinctive fake secret so you can spot it in the echo
keyward add echotest --endpoint httpbin.org
# at the prompt, enter: sk-fake-secret-12345
keyward restart # only needed if a LaunchAgent daemon is already running
keyward run -- curl -s "$ECHOTEST_BASE_URL/anything" \
-H "Authorization: Bearer $ECHOTEST_API_KEY"
In the JSON response, under headers.Authorization:
Bearer sk-fake-secret-12345means the swap worked.- Anything starting with
Bearer kw_means the swap did not happen (bug).
For the Anthropic-style (x-api-key):
keyward add echotestx --endpoint httpbin.org --auth-style x-api-key
keyward restart
keyward run -- curl -s "$ECHOTESTX_BASE_URL/anything" \
-H "x-api-key: $ECHOTESTX_API_KEY"
Check headers.X-Api-Key in the response.
Clean up with keyward rm echotest -y && keyward rm echotestx -y.
There is also a Python equivalent that uses keyward.activate():
keyward add echotest --endpoint httpbin.org
keyward restart
uv run python scripts/verify_swap.py echotest
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keyward-1.1.1.tar.gz.
File metadata
- Download URL: keyward-1.1.1.tar.gz
- Upload date:
- Size: 99.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e9a712d936979f106a9df76875e9b424a4a7d33e04f586460616a37789d1a657
|
|
| MD5 |
939eb0de60796cf2d05a22884f087700
|
|
| BLAKE2b-256 |
6dc224c69e619803e644d460097a643a0a138b94ac16c6b81b0d2025c40a51d8
|
Provenance
The following attestation bundles were made for keyward-1.1.1.tar.gz:
Publisher:
release.yml on sumedhrasal/keyward
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
keyward-1.1.1.tar.gz -
Subject digest:
e9a712d936979f106a9df76875e9b424a4a7d33e04f586460616a37789d1a657 - Sigstore transparency entry: 1697799694
- Sigstore integration time:
-
Permalink:
sumedhrasal/keyward@10bc156784de113a359f7799b73eee1fec46c282 -
Branch / Tag:
refs/tags/v1.1.1 - Owner: https://github.com/sumedhrasal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@10bc156784de113a359f7799b73eee1fec46c282 -
Trigger Event:
release
-
Statement type:
File details
Details for the file keyward-1.1.1-py3-none-any.whl.
File metadata
- Download URL: keyward-1.1.1-py3-none-any.whl
- Upload date:
- Size: 19.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8174193f8ec0ca2492e657f3f6057a72d0be158d5e469c4707ada20b158a02a0
|
|
| MD5 |
f35ee373d9c1f064ee6e0f2d0f53fdda
|
|
| BLAKE2b-256 |
2bafe26c7e5e7b385e4e3642e8829de1ecc026932df676d74d9c0bc0c49a12b2
|
Provenance
The following attestation bundles were made for keyward-1.1.1-py3-none-any.whl:
Publisher:
release.yml on sumedhrasal/keyward
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
keyward-1.1.1-py3-none-any.whl -
Subject digest:
8174193f8ec0ca2492e657f3f6057a72d0be158d5e469c4707ada20b158a02a0 - Sigstore transparency entry: 1697799831
- Sigstore integration time:
-
Permalink:
sumedhrasal/keyward@10bc156784de113a359f7799b73eee1fec46c282 -
Branch / Tag:
refs/tags/v1.1.1 - Owner: https://github.com/sumedhrasal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@10bc156784de113a359f7799b73eee1fec46c282 -
Trigger Event:
release
-
Statement type: