frameworkcredentials-policy · v31099:claude-chat-c1d32026-11-10All documents

Credentials policy — where secrets live and how agents handle them

Credentials policy

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.


Credentials

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.

Where each credential lives

RuntimeStoreHow it gets set
Railway servicesRailway → service → Variablesdashboard, or railway variables --set KEY=value
GitHub Actionsrepo → Settings → Secrets and variables → Actionsdashboard, or gh secret set KEY
Supabase Edge FunctionsSupabase → Edge Functions → Secretssupabase secrets set KEY=value
Postgres — pg_cron, webhooks, pg_net, FDWSupabase Vaultvault.create_secret('value','name','desc')
Local developmenta gitignored .envcopy .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.

Rules — treat these as absolute

  1. Read credentials only through the environment. process.env.NAME, Deno.env.get('NAME'), os.environ['NAME']. Never inline a literal key, not even one that looks like a placeholder.
  2. Never emit a secret value anywhere. Not in logs, console output, commit messages, PR bodies, code comments, error text, test fixtures, or a reply to the human. Not truncated, not partially masked, not "just the first six characters."
  3. Never open a file whose job is to hold raw credentials — .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.
  4. A missing variable is a hard failure. Before you report one as missing, confirm the name is absent from the runtime stores in the table above (list names only, never values). Then throw immediately with a message naming the variable and the store it belongs in. Never substitute a default, never skip the auth path, never disable a check to make a test pass, never silently degrade to an unauthenticated call.
   const key = process.env.SUPABASE_SERVICE_KEY;
   if (!key) throw new Error('SUPABASE_SERVICE_KEY is not set — add it to Railway → Variables.');
   
  1. Never widen a secret's blast radius. No backend key reaching a client bundle. No 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.
  2. A credential found in the repo or in a working file is an incident. Stop, tell the human, and say plainly that the key must be rotated, not just deleted — a deleted key is still in git history and still valid. Do not attempt the rotation yourself.
  3. If the human offers you raw keys, decline. Anything you read enters the transcript and can echo into a file, a log line, or a commit. Ask them to put the value in the appropriate store above and tell you only the variable name. Example reply: "Please set GROQ_API_KEY in Railway → Variables and tell me when it's there; I only need the name."

Client-safe vs backend-only

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.

Naming

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.

Change log

Every change to this framework: who, when, why. Source: agent_ops.catalog_changelog.

When (UTC)WhoVersionWhatWhy
2026-11-10 10:361099:claude-chat-c1d3v3Legacy 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:161099:claude-chat-c1d3v2Credentials policy — where secrets live and how agents handle themOwner-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:021099:claude-chat-c1d3v1Credentials policy — where secrets live and how agents handle themnot recorded (before change log started 2026-11-08)