North star: No credential value ever enters a repo, transcript, log or client bundle, while agents still find the variable names they need and keep working. Hard gates (never bend): never emit or read secret values; no secrets in repo or client bundle; leaked key means rotate; publishable key only in browsers; decline raw keys from humans Everything else is a default. Try a better or unusual way when it serves the north star; record what you changed and why in the change log. Change log: bottom of this page (who, when, why).
Paste this section into the CLAUDE.md of every project (or link it — the directive at https://hi.jbnx.io keeps always-apply files to a pointer). It tells any agent where credentials live, and — more importantly — that there is no key file to go looking for.
There are no secrets in this repository and none on the developer's disk. If you are searching for a key file, stop: you are about to do the wrong thing. Every credential lives in the runtime's own encrypted store and is injected as an environment variable at run time.
| Runtime | Store | How it gets set |
|---|---|---|
| Railway services | Railway → service → Variables | dashboard, or railway variables --set KEY=value |
| GitHub Actions | repo → Settings → Secrets and variables → Actions | dashboard, or gh secret set KEY |
| Supabase Edge Functions | Supabase → Edge Functions → Secrets | supabase secrets set KEY=value |
Postgres — pg_cron, webhooks, pg_net, FDW | Supabase Vault | vault.create_secret('value','name','desc') |
| Local development | a gitignored .env | copy .env.example and fill it in yourself |
.env.example is the registry of every variable this project needs. It lists names and never values. When you introduce a new credential, add its name and a one-line comment there in the same commit — that file is how the next person and the next agent learn the variable exists.
process.env.NAME, Deno.env.get('NAME'), os.environ['NAME']. Never inline a literal key, not even one that looks like a placeholder..env, .env. (but .env.example is the names-only registry: open it freely), .pem, .key, id_rsa, credentials.json, .npmrc, .netrc, anything under .aws/, .ssh/, or a path containing secret. You need the variable's name* to write correct code. You never need its value. const key = process.env.SUPABASE_SERVICE_KEY;
if (!key) throw new Error('SUPABASE_SERVICE_KEY is not set — add it to Railway → Variables.');
NEXT_PUBLIC_ or VITE_ prefix on anything secret — that prefix is a publication instruction. No logging a request that carries an Authorization header. No passing a key to a third-party service that was not already trusted with it.GROQ_API_KEY in Railway → Variables and tell me when it's there; I only need the name."Exactly one class of key may appear in a browser bundle: Supabase's publishable key (sb_publishable_..., formerly anon). It is RLS-enforced and safe to expose.
Everything else is backend-only: the Supabase secret key (sb_secret_..., formerly service_role, which bypasses RLS entirely), and every model-vendor and third-party API key. These belong in a server, an Edge Function, or a CI job — never in code the browser downloads. Before shipping, grep the production bundle for sb_secret_, service_role, and each vendor's key prefix.
Variable names are SCREAMING_SNAKE_CASE and start with the service: SUPABASE_SERVICE_KEY, GROQ_API_KEY, RAILWAY_TOKEN.
The SUPABASE_ prefix is reserved inside Supabase Edge Function secrets — the platform rejects your own variables with that prefix, and injects its own (SUPABASE_URL, SUPABASE_SECRET_KEYS, and others). Outside Supabase — in Railway or GitHub Actions — you may name variables whatever you like, including SUPABASE_SERVICE_KEY.
One migration trap: the injected SUPABASE_SECRET_KEYS and SUPABASE_PUBLISHABLE_KEYS hold JSON objects keyed by name, not the plain strings that SUPABASE_SERVICE_ROLE_KEY and SUPABASE_ANON_KEY held. Code written against the old vars breaks silently:
const keys = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!);
const admin = createClient(Deno.env.get('SUPABASE_URL')!, keys['default']);
Legacy anon / service_role JWTs still work; Supabase docs say they "will be deprecated shortly" and give no date (checked 2026-11-10). Prefer the new format for anything you write now.
Directive context: https://hi.jbnx.io §5 (live host decides) and /ref/R4 (how to ask). Source: CEO, 2026-09-18.
Every change to this framework: who, when, why. Source: agent_ops.catalog_changelog.
| When (UTC) | Who | Version | What | Why |
|---|---|---|---|---|
| 2026-11-10 10:36 | 1099:claude-chat-c1d3 | v3 | Legacy key deprecation line now quotes Supabase docs (no date given). | Stale claim: page said legacy keys deprecated by end of 2026; Supabase docs give no date, so agents could plan on a false deadline. |
| 2026-11-08 16:16 | 1099:claude-chat-c1d3 | v2 | Credentials policy — where secrets live and how agents handle them | Owner-requested north-star review 2026-11-08: north star + hard gates header, top fixes, non-safety rules loosened to defaults (details in the page change log) |
| 2026-09-18 07:02 | 1099:claude-chat-c1d3 | v1 | Credentials policy — where secrets live and how agents handle them | not recorded (before change log started 2026-11-08) |