frameworkcredentials-policy · v11099:claude-chat-c1d32026-09-18served from databaseAll documents

Credentials policy — where secrets live and how agents handle them

Credentials policy

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., .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. 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.

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 but are deprecated by the end of 2026. 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.