feat(config): project-local config discovery for CLI and SDKs - #866
feat(config): project-local config discovery for CLI and SDKs#866kcoopermiller wants to merge 16 commits into
Conversation
Resolve the config directory as PRIME_CONFIG_DIR > nearest ancestor of the working directory containing .prime/config.json > ~/.prime, so credentials can live next to a project (e.g. on a shared machine) instead of only in the home directory. Applies to the CLI and the sandboxes, evals, tunnel and traces SDKs; every Config() call site is no-arg so all of them pick it up. - A project-local config is a whole replacement for the global one, never merged with it, so a planted local file cannot pair its base_url with the user's global API key. The walk stops at $HOME and ignores files not owned by the current uid. - `prime login --local` / `prime config set-api-key --local` create ./.prime/config.json (mode 0600) and warn when the repo does not gitignore .prime/. `prime config view` shows which file is active. - Config and environment files are now written owner-only (0600). - Test conftests start each test in a tmp working directory so discovery never reaches the developer's real ~/.prime. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 91ab831a36
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
CI exports a PRIME_API_KEY secret for the sandboxes job, and env vars outrank the config file by design, so the discovery test that expected the file's key saw the secret instead. Drop the var in the test isolation fixtures (sandboxes, evals, tunnel) alongside PRIME_CONFIG_DIR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
Address review findings on project-local config discovery: - Ownership is not enough to trust a discovered .prime/config.json: one committed to a repository the user clones is owned by the user and could redirect an environment-provided PRIME_API_KEY to an attacker's base_url. Discovered local configs are now ignored (with a one-line warning) until approved via `prime config trust`, recorded in ~/.prime/trusted_configs.json as path + content digest so a changed file must be re-trusted. The CLI's own writes (`--local` flows, set-*) refresh the trust entry; PRIME_CONFIG_DIR and explicit Config(config_dir=...) remain trusted as such. Untrusted candidates are skipped rather than ending the search, so a nested planted file cannot mask the user's trusted one. `prime config untrust` withdraws approval; `prime config view` lists ignored files. - `prime login --local` / `set-api-key --local` no longer write a default file before authentication succeeds (Config(create=False)); an aborted login leaves nothing behind that would shadow the global config. A new local config inherits the service URLs of the currently active config instead of resetting to production. - The gitignore warning walks up to the enclosing repository root and asks `git check-ignore` (falling back to scanning .gitignore files), so running `--local` from a subdirectory of a repo is covered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
`prime login --local` / `set-api-key --local` loaded whatever ./.prime/config.json was already there and, on writing the key, recorded trust for the whole file — so a config planted in a cloned repository would have its base_url adopted (and the login challenge sent to it). Refuse an existing untrusted file with a pointer to `prime config trust`; a trusted one is still updated in place. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
The untrusted-file guard treated ~/.prime/config.json as a project-local candidate when run from $HOME; the global config is never in the trust registry, so `--local` there was refused. Recognize the global dir, skip the trust check and the project-local notice for it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
`Config.is_global` compared unresolved paths while the --local guard resolved them. Path.cwd() is always physical and Path.home() may go through a symlink, so with a symlinked home `--local` from $HOME printed the project-local notice and entered ~/.prime/config.json in the trust registry. Route both through one resolved-path helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b1e298f769
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
On a shared machine a trusted path could be swapped for someone else's file carrying the same bytes; the digest check alone still accepted it and `--local` would write the new key into it before `_record_trust` noticed. `is_trusted_local_config` (CLI and SDKs) now also requires the file to be owned by the current user, `--local` refuses a foreign-owned existing file up front, and `_write_private_json` refuses to write into any file — or through any symlink — not owned by the current user, so O_TRUNC can't be turned into a write to wherever a planted symlink points. User-owned dotfile-style symlinks keep working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bf57d55d56
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
With a trusted project config, PRIME_CONTEXT / `prime config use` loaded .prime/environments/<name>.json unchecked, so a repository update could rewrite an environment's base_url while config.json stayed trusted. Trust is now recorded per file: `prime config trust` approves config.json and its environment files together, CLI writes to environment files refresh their entries, `untrust` drops them, and a discovered local config refuses untrusted environment files (PRIME_CONTEXT warns and skips; `use` errors). Global and explicit config dirs are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
`_environment_file_is_trusted` only applied to discovered ("local")
configs, but `Config.local()` used by `login --local` is "explicit", so
PRIME_CONTEXT could still load a planted ./.prime/environments/<name>.json
and send the auth challenge to its base_url. Gate every non-global config
dir except an explicit PRIME_CONFIG_DIR, mirroring `_record_trust`.
`set_traces_url_for_active_environment` also read-modified-wrote the
environment file and then re-trusted it, which would launder a rewritten
base_url into trust; it now refuses an untrusted environment file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
|
@codex review |
…nfig `set_traces_url_for_active_environment` wrote config.json and only then refused an untrusted environment file, leaving traces_url persisted (and re-trusted) on the root despite the command failing. Check the environment file first so a refusal leaves no half-applied state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be971300be
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…gout trusted A cloned repository can ship `.prime/config.json -> ../leak.json` (dangling, so the path looks absent) or a symlinked environments/<name>.json; the credential write would follow it and create a key-bearing file outside the ignored .prime/ directory, or clobber an existing target. Project-local config dirs now never write through symlinks (dangling or not) and `--local` refuses a symlinked config.json up front; `prime config trust` skips symlinked environment files. The global ~/.prime still follows user-owned symlinks to an existing target, for dotfile setups, but refuses dangling ones. `prime logout` rewrote the active environment file with a plain write_text, so on a project-local config its trust digest went stale and the next `prime config use` was refused. Route it through Config.update_current_environment_file(from_env=False), which does the owner-only write and refreshes the trust entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
|
@codex review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a2bef34. Configure here.
…inked files The symlink guards only inspected the leaf, so a cloned repository could ship `.prime` or `.prime/environments` as a directory symlink and still redirect credential writes outside the ignored tree. And a symlinked file whose target was registered still counted as trusted (the registry keys on the resolved path), so discovery or set-traces-url accepted it and the later write refused after the root file was already updated. One rule now covers project-local files: if the file, its directory, or its .prime directory is a symlink it is never trusted, never recorded by `prime config trust`, never a candidate environment file, and never written to — and `--local` refuses it up front. The SDK copies mirror the "never trusted" half so CLI and SDKs agree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a2bef34533
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
With a trusted project config whose current_environment is a named one, a checkout could rewrite the (possibly tracked) environments/<name>.json; the load path refused it, but update_current_environment_file() — run as a side effect of login / set-* / logout — overwrote it with the fresh API key and recorded the new digest as trusted, and `prime config save` did the same. Both now check trust first: the implicit path skips the file with a one-line warning so the command still succeeds, and `save` fails with an explicit error pointing at `prime config trust`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ab76c3842
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`prime config delete <name>` unlinked environments/<name>.json through whatever the environments/ directory resolved to, so a checkout that turned it into a symlink would delete an attacker-chosen external file. Apply the same involves_symlink rule as the write path for project-local configs, and drop the file's trust entry when it is deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 540d56027a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…mp file `_write_private_json` opened the target with O_CREAT|O_TRUNC and chmod'ed afterwards, so a legacy 0644 config.json held the new API key between the write and the chmod, and readers of trusted_configs.json (every SDK Config() loads it) could observe an empty or partial file while the CLI was refreshing an entry, silently falling back to the global config. Write to a fresh 0600 temporary file in the same directory and os.replace it over the target: a permissive file is replaced rather than rewritten, and readers see either the old or the new content. User-owned symlinks in the global config dir are honored by replacing their target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49955611ba
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Registry writes were atomic but the load-modify-save around them was not, so two CLI processes refreshing different projects could each load the same registry and the later replace dropped the other's entry, making that project silently fall back to the global config. Every mutation now runs under an flock on a sibling lock file (no-op on Windows); the registry itself can still be atomically replaced while the lock is held. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw

Summary
Lets the CLI and SDKs use project-local credentials instead of only
~/.prime/config.json— e.g. on a shared box where all your code lives underprime/, put credentials atprime/.prime/config.jsonand everything run from there (or any subdirectory) uses them.Config directory resolution, for the CLI and the
prime-sandboxes,prime-evals,prime-tunnel, andprime-tracesSDKs:PRIME_CONFIG_DIR(explicit).prime/config.json~/.prime(unchanged default)Per-field env vars (
PRIME_API_KEY,PRIME_TEAM_ID, …) still take precedence over either file. AllConfig()call sites are no-arg, so every command and SDK client picks this up automatically.Details
direnv allow. A discovered.prime/config.jsonis ignored (one warning line) untilprime config trustrecords it in~/.prime/trusted_configs.jsonas resolved path → sha256 of content. A file that changes under the user (e.g.git pull) must be re-trusted. This is what stops a config committed to a cloned repository from pointing an environment-providedPRIME_API_KEYat someone else'sbase_url. Files the CLI writes itself (--localflows,set-*) refresh the entry;PRIME_CONFIG_DIRand explicitConfig(config_dir=…)are trusted as such. Untrusted candidates are skipped rather than ending the search, so a nested planted file can't mask the user's trusted one further up.prime config untrustwithdraws approval.base_urlsend the user's global API key elsewhere.$HOME, and files not owned by the current uid are never candidates. Skipped on Windows, which has no uid model.login --local/set-api-key --localuseConfig(create=False): the local file only appears on the firstset_*call, so an aborted login can't leave an empty file that shadows the global config. A new local config inherits the service URLs of the currently active config instead of resetting to production.git check-ignoreabout the real path (with a.gitignorescan fallback), so--localfrom a subdirectory of a repo is covered.prime-tunnel's frpcbin_dirdeliberately stays under~/.prime.Configcopies are kept dependency-free, so the discovery/trust helpers are duplicated into each (same pattern as the rest of those files). SDKs only read the trust registry; the CLI maintains it.Tests
test_config_discovery.pyin all five packages: global fallback, cwd/ancestor discovery, nearest-wins, untrusted ignored with warning, nested-untrusted doesn't mask trusted, trust bound to content, no-merge, env-var precedence,PRIME_CONFIG_DIRoverride, stop-at-home, ownership check, writes go to the discovered file and refresh trust,create=Falsedefers writes,adopt_urls,trust/untrustCLI,--localflows incl. URL inheritance, abortedlogin --localleaves nothing, gitignore warning from a nested directory of a realgit initrepo.conftest.pyin each package starts every test in a tmp working directory (and dropsPRIME_CONFIG_DIR/PRIME_API_KEY). Without this, discovery run from a checkout under$HOMEwould reach the developer's real~/.prime, and tests that patchHOMEwould then read and write it.prime1051 passed ·prime-sandboxes108 ·prime-evals20 ·prime-tunnel34 ·prime-traces155;ruffandtyclean.$HOME: hand-written local config ignored →prime config trust→ used → file edited → ignored again; abortedlogin --localleaves no.prime/.🤖 Generated with Claude Code
https://claude.ai/code/session_014VNXu8DW3Ln4kmr6CGmWvw
Note
High Risk
Changes how API keys and base URLs are resolved across the CLI and all SDKs; mistakes could misroute credentials or break existing setups that rely only on
~/.prime.Overview
Adds project-local credentials so the CLI and Python SDKs can use
./.prime/config.json(fromprime login --local/prime config set-api-key --local) instead of only~/.prime. Resolution isPRIME_CONFIG_DIR→ nearest trusted ancestor.prime/config.json→~/.prime; a local file fully replaces the global one (no merge).PRIME_*env vars still win over files.Trust (direnv-style): discovered project configs are ignored until
prime config trustrecords path → SHA-256 in~/.prime/trusted_configs.json; content changes require re-trust. CLI addsconfig trust/untrust, richerconfig view, gitignore warnings for--local, and deferred writes (create=False) so failed login does not leave an empty local config. Hardening: uid ownership on candidates, no project-local writes through symlinks, same trust rules for.prime/environments/*.json, owner-only atomic 0600 JSON writes, and flock on the trust registry.The same discovery helpers are applied in
prime_cliand each SDK (prime-evals,prime-sandboxes,prime-traces,prime-tunnel);prime-tunnelkeeps frpcbin_dirunder~/.prime. Docs and broadtest_config_discovery+ hermeticconftestfixtures cover the new behavior.Reviewed by Cursor Bugbot for commit 826318d. Bugbot is set up for automated code reviews on this repo. Configure here.