Secrets
A secret is a token, key or password that an MCP server or hook needs. palm writes a reference to an environment variable in its place, in each harness’s own syntax, so the files you commit hold no values.
$ palm install mcp --snippet - <<'JSON'{ "mcpServers": { "inbound": { "url": "https://mcp.inbound.example/v1", "headers": { "x-inbound-api-key": "inbound-live-Zx8kQ2mN7pL4vR9tW3yB6cF1" } } } }JSON+ mcp inbound .mcp.json, .cursor/mcp.json merged (needs INBOUND_API_KEY in the environment)1 installed. Commit palm.yaml, palm.lock.yaml, .mcp.json and .cursor/ together.
Warnings ! inbound: headers.x-inbound-api-key held a literal secret; written as ${INBOUND_API_KEY}; export INBOUND_API_KEY=… before starting Claude Code or Cursor$ grep -n INBOUND .mcp.json .cursor/mcp.json palm.yaml.mcp.json:7: "x-inbound-api-key": "${INBOUND_API_KEY}".cursor/mcp.json:6: "x-inbound-api-key": "${env:INBOUND_API_KEY}"palm.yaml:5: headers: {x-inbound-api-key: "${INBOUND_API_KEY}"}The README snippet held a live key.
palm wrote ${INBOUND_API_KEY} into palm.yaml and the Claude Code config, ${env:INBOUND_API_KEY} into the Cursor config, and named the variable to export.
What palm treats as a secret
Section titled “What palm treats as a secret”palm scans MCP env values, headers, arguments and URLs, hook commands, every script in an asset directory and palm.yaml itself.
It scans at index time, before it writes, and again in palm check.
| Shape | Examples |
|---|---|
| a known prefix | sk-, ghp_, github_pat_, gho_, xoxb-, xoxa-, xoxp-, AKIA, AIza, glpat-, -----BEGIN |
| a bearer token | Bearer eyJhbGciOi... |
| a high-entropy value under a telling key | 20 or more characters, above 3.5 bits of entropy per character, under a key named with the whole word key, api key, token, secret, password or authorization, such as x-api-key; keywords does not count |
Variables a harness fills in at run time, such as CLAUDE_PLUGIN_ROOT, CLAUDE_PROJECT_DIR, workspaceFolder or HOME, are never secrets. palm leaves them as written.
Code is not a value either. An identifier or expression such as process.env.API_KEY, queryKey(), React.useContext(TokenContext) or token || null is never a finding.
In the files a skill, agent or instruction copies, a known prefix, a bearer token or a private key block refuses the entity, and so does a high-entropy value in a configuration file such as .json, .yaml or .env.
A high-entropy value in code or prose only warns, because palm cannot tell a random string from a key there.
A finding in a test folder the skill copy leaves out, such as tests/ or fixtures/, never refuses.
What palm decides
Section titled “What palm decides”| The literal came from | Project scope | Global scope (-g) |
|---|---|---|
| a source | never written; palm writes ${NAME} and says so |
never written; palm writes ${NAME} and says so |
| a README snippet you pasted | replaced by ${NAME}, with a notice |
replaced by ${NAME}, with a notice |
you, with --env K=V, --header K=V or --arg, or in palm.yaml |
a secret is written as a reference, such as ${K}, and palm prints the export line; with --secrets literal, written, with a warning when git would commit the file |
a secret is written as a reference; with --secrets literal, written only when the file’s real path is outside every git worktree |
Under the default policy, a value you type is a secret when its key says so (API_KEY, Authorization) or the value is shaped like one.
palm writes the reference, prints the export line to run before starting the harnesses, and keeps the value out of every file.
A value that is no secret, such as LOG_LEVEL=debug, is written as you typed it, and an i line says so.
An --arg after a flag named like a secret, such as --arg --api-key --arg <value>, becomes a reference whole.
A secret in a URL becomes a reference for that part alone: https://mcp.example.com/v1?key=${REMOTE_KEY} keeps the host readable, and the variable is named after the parameter.
A source has no business shipping literal values: a high-entropy value under any env key or header a source ships, such as CREDENTIALS or X-Session, becomes ${KEY} as well.
The consent review marks an env value a program still gets as written, such as LOG_LEVEL (literal), so you see it before you allow the program.
--secrets literal is the only way to store a value, and palm records it on the entry as secrets: literal, so the next bare install keeps it.
palm reads each value from the environment, and asks with a masked prompt only on a terminal.
Every harness file that can hold secrets is created with mode 0600 in the global scope, and a file that receives a literal is set to 0600.
A global literal write into a dotfiles checkout is refused, because the file’s real path is inside a git worktree.
palm follows symlinks to decide, and --force never overrides the refusal, nor a literal that came from a source.
Placeholders and VS Code inputs
Section titled “Placeholders and VS Code inputs”README snippets often hold a value to fill in, such as "Authorization": "Bearer YOUR_API_KEY".
palm treats YOUR_..., <your-...>, xxx... and changeme under a secret-shaped key as a placeholder, never as a value.
It writes an environment reference instead, and a notice names the variable.
In a header the reference is optional and named after the header, as a source’s is, such as Bearer ${CONTEXT7_TOKEN:-}: the server works without the key until you export one.
A VS Code snippet refers to its inputs list, as in ${input:github_mcp_pat}.
palm writes ${GITHUB_MCP_PAT} in its place, drops the inputs array, and says so for each.
One reference, six spellings
Section titled “One reference, six spellings”| Harness | File | Authorization: Bearer ${DOCS_TOKEN} becomes |
|---|---|---|
| Claude Code | .mcp.json |
Bearer ${DOCS_TOKEN} |
| Codex | .codex/config.toml |
bearer_token_env_var = "DOCS_TOKEN" |
| GitHub Copilot | .vscode/mcp.json |
Bearer ${env:DOCS_TOKEN} |
| Cursor | .cursor/mcp.json |
Bearer ${env:DOCS_TOKEN} |
| Gemini CLI | .gemini/settings.json |
Bearer ${DOCS_TOKEN} |
| OpenCode | opencode.json |
Bearer {env:DOCS_TOKEN} |
Codex has no ${VAR} syntax in its config.
palm uses bearer_token_env_var for a bearer token, env_http_headers for other headers and env_vars for a stdio server’s environment.
Claude Code refuses a config whose variable is unset and has no default, so an optional secret becomes ${VAR:-} there.
An HTTP server without declared secrets gets its URL only. The harness runs the OAuth sign-in on first connect, and palm stores no token.
What palm keeps
Section titled “What palm keeps”The index and the lock store a redacted hash for a secret-shaped value, never the value.
palm’s cache directory is created with mode 0700.
--dry-run makes the same decision and prints it, such as would write the literal value of BRAVE_API_KEY into ~/.codex/config.toml (mode 600).
When a secret is already committed
Section titled “When a secret is already committed”When palm replaces a literal it finds in a generated file, it tells you to rotate the value. It adds that the old value stays in git history only when git tracks the file.
! inbound: .cursor/mcp.json held a literal value for x-inbound-api-key (tracked in git). palm replaced it with ${INBOUND_API_KEY}. The old value stays in git history: rotate it. export INBOUND_API_KEY before starting Cursor.palm check fails on a secret-shaped literal in a file git tracks or others can read, following symlinks.
It scans every harness config palm merges into, such as .cursor/mcp.json, and flags any literal env or header value there, blamed on the server at its JSON pointer; a server palm did not write is called foreign.
A literal palm wrote under --secrets literal is palm’s own, so the check right after such an install passes.
High-entropy text in code only warns.
Its variables check lists every variable the installed servers need, with one warning line per server when some are not set.