feat(profiles): named auth profiles for multi-account isolation#102
Conversation
Add a profile system so claudetm can run under isolated credentials
instead of the single global ~/.claude/.credentials.json. This enables
switching between Claude subscriptions (or a custom Anthropic-compatible
endpoint) and running several in parallel.
Two profile types:
- oauth: isolated CLAUDE_CONFIG_DIR per profile under
~/.claudetm/profiles/<name>/, so subscriptions never clobber
each other's credentials/token refresh.
- api-key: ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL injection (e.g. z.ai/GLM
via an Anthropic-compatible endpoint).
Implementation:
- core/profiles.py: ProfileManager (registry at ~/.claudetm/profiles.json,
overridable via CLAUDETM_HOME), Profile model, and resolve_runtime_env()
which maps the active profile (or CLAUDETM_PROFILE override) to the env a
run launches with. Defensive: any failure falls back to default creds.
- Inject the resolved env at the SDK subprocess boundary in
core/agent_query.py and core/conversation.py.
- CredentialManager: read the active oauth profile's config dir (path is now
an instance property; the patchable CREDENTIALS_PATH default is preserved),
and short-circuit the OAuth file check for api-key profiles.
- CLI: `claudetm profile add/list/use/show/remove/login`.
- Tests for the manager, env resolution, and profile-aware credentials.
- Docs in README.md and CLAUDE.md, including the same-account refresh-token
caveat for parallel runs.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds named oauth and api-key profiles, registry-backed profile management, profile-aware credential lookup, runtime env injection into subprocesses, and a new ChangesMulti-account Profile Management
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 markdownlint-cli2 (0.22.1)README.mdmarkdownlint-cli2 wrapper config was not available before execution Comment |
CI runs mypy over the whole tree; assert config_dir is not None before constructing a Path from it.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/claude_task_master/cli_commands/profile.py`:
- Around line 56-60: The api-key path in the profile add CLI currently accepts
secrets via the --api-key option, which exposes them in shell history and
process listings; update the profile add flow in the profile.py command handlers
to gather api_key through a hidden prompt and/or environment variable input
instead of a command-line argument, and adjust any related examples/help text to
show the safer input method. Focus on the profile add command and its api-key
profile creation logic so the secret is never passed as a visible CLI flag.
In `@src/claude_task_master/core/credentials.py`:
- Around line 169-181: The profile resolution path in `CredentialsManager` is
silently falling back to `~/.claude` when `ProfileManager.resolve_active()`
fails, which can verify the wrong credentials. Update the `config_dir is None`
branch so failures from `ProfileManager` do not default to the global path;
instead, propagate or explicitly handle the active-profile lookup failure and
only set `_profile` / `_config_dir` when a valid profile is resolved. Keep the
logic around `resolve_active`, `_profile`, and `_config_dir` aligned with the
multi-profile behavior so OAuth reads from the selected profile and `api-key`
profiles still bypass the OAuth file check.
In `@src/claude_task_master/core/profiles.py`:
- Around line 202-207: The persistence paths in ProfileRegistry.save and the
related oauth profile directory creation need tighter permissions before writing
secrets. Update the registry file write flow so the temp file and final
profiles.json are created with owner-only access instead of relying on the
process umask, and apply the same hardening to the profile home directory used
for .credentials.json. Use the existing save method and the oauth/profile
directory setup code to ensure permissions are explicitly locked down before any
credential data is stored.
- Around line 151-158: The profile resolution path in the profile env helper is
swallowing all failures and returning an empty env, which hides bad explicit
selections and registry problems. Update the logic around
ProfileManager.resolve_active and env_for_profile so that {} is returned only
when no active profile is selected, while invalid CLAUDETM_PROFILE values or
ProfileManager/registry errors are allowed to fail fast or be surfaced. Keep the
existing no-profile fallback behavior intact, but do not catch and mask all
exceptions in the profile-loading flow.
- Around line 281-285: Validate the profile name before using it in Profiles
setup: the oauth branch in the profile creation flow currently turns name into a
path via the profile_home logic, which allows path traversal or absolute paths
to escape the profiles directory. Add a strict name check in the profile
handling code before profile_home is built, and only allow safe relative names
that stay under self.profiles_dir; reject or normalize anything containing
separators, dot segments, or absolute paths.
In `@tests/core/test_profiles.py`:
- Around line 165-170: The profile resolution in resolve_runtime_env() is
currently treating an unknown CLAUDETM_PROFILE as empty env, which can silently
fall back to the global ~/.claude session and break isolation. Update the
resolution path in resolve_runtime_env() (and any helper it uses) to either fail
fast on unknown profile names or fall back to the persisted active profile from
ProfileManager instead of returning {}. Then adjust
test_unknown_override_falls_back_to_default to assert the chosen safe behavior
rather than an empty runtime env.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 57af1919-aec4-49eb-89bb-468a4d859616
📒 Files selected for processing (10)
CLAUDE.mdREADME.mdsrc/claude_task_master/cli.pysrc/claude_task_master/cli_commands/profile.pysrc/claude_task_master/core/agent_query.pysrc/claude_task_master/core/conversation.pysrc/claude_task_master/core/credentials.pysrc/claude_task_master/core/profiles.pytests/core/test_credential_profiles.pytests/core/test_profiles.py
…ety, secret input Security hardening from review: - Don't mask profile-resolution failures. resolve_active() now raises ProfileNotFoundError when a profile is explicitly selected (CLAUDETM_PROFILE or the persisted active pointer) but missing; resolve_runtime_env() and CredentialManager no longer catch-all and silently fall back to the global ~/.claude credentials (which could green-light the wrong account). Empty env is returned ONLY when no profile is selected. - Lock down credential storage: profiles.json (which holds raw API keys) and oauth profile homes are created owner-only (0o600 / 0o700) via explicit chmod rather than relying on the process umask. - Validate profile names before they become filesystem paths: reject path separators, dot segments, and unsafe charsets, with a resolved-path guard as defense in depth against traversal. - Never accept the API key as a visible CLI flag. `profile add` now reads it from CLAUDETM_API_KEY or a hidden prompt, keeping it out of shell history and process listings. Docs/examples updated. Tests updated/added: fail-fast on unknown profile, name validation, and owner-only permissions.
|
Thanks @coderabbitai — all 6 findings addressed in
|
|
Good fixes across the board, especially:
Appreciate the clear breakdown and the green 🐇✨ ✏️ Learnings added
✅ Action performedComments resolved and changes approved. |
## Summary Release **v0.1.50** — bumps the version metadata for the authentication profiles feature (#102). ## Changes - `pyproject.toml` + `src/claude_task_master/__init__.py`: `0.1.49` → `0.1.50` - `CHANGELOG.md`: add the `[0.1.50]` entry (profiles: multi-account isolation, `claudetm profile` CLI, per-profile SDK env injection). ## Testing - Version metadata only; no code change. Profiles feature itself shipped and was tested in #102. > Note: the `v0.1.50` tag is already pushed and PyPI publish (OIDC) is triggered from it; this PR lands the version bump on `main` so the branch metadata matches the release. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for multiple authentication profiles, including new command-line options and updated profile selection behavior. * Improved how authentication settings are resolved from the environment, local configuration, and directories. * Added clearer handling for profile-related errors. * **Documentation** * Updated the release notes for version 0.1.50 and refreshed version references. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: marshall <marshall@developerz.ai>
Summary
Adds a profile system so claudetm can run under isolated credentials instead of the single global
~/.claude/.credentials.json. This lets you switch between Claude subscriptions (or a custom Anthropic-compatible endpoint such as z.ai/GLM) and run several in parallel without them colliding.Why
Today every run reads/refreshes the one global OAuth credentials file. That makes it impossible to (a) keep multiple subscriptions side by side, or (b) run two of them at the same time — the in-place token refresh of one clobbers the other. Profiles isolate each account's credentials into its own directory.
Profile types
oauth— an isolatedCLAUDE_CONFIG_DIRper profile under~/.claudetm/profiles/<name>/. The bundledclaudeCLI reads and refreshes credentials inside that directory, so two oauth profiles never clobber each other.api-key— a direct API key + base URL injected asANTHROPIC_API_KEY/ANTHROPIC_BASE_URL(e.g. z.ai / GLM via an Anthropic-compatible endpoint).Implementation
core/profiles.py—ProfileManager(registry at~/.claudetm/profiles.json, base dir overridable viaCLAUDETM_HOME), theProfilemodel, andresolve_runtime_env()which maps the active profile (or a per-runCLAUDETM_PROFILEoverride) to the environment a run launches with. Deliberately defensive: any failure (missing/corrupt registry, unknown override) falls back to the default credentials so existing behavior is preserved.core/agent_query.py(single-turn queries) andcore/conversation.py(multi-turn). No-op when no profile is active.core/credentials.py—CredentialManagernow resolves the active oauth profile's config directory. The credentials path is an instanceproperty; the patchableCREDENTIALS_PATHclass default is preserved for backwards compatibility. Forapi-keyprofiles the OAuth-file pre-flight is short-circuited (those authenticate via env).claudetm profile add/list/use/show/remove/login.loginlaunchesclaudewith the profile'sCLAUDE_CONFIG_DIRso you can/logininto an isolated session.Usage
Parallelism note
Isolation is between different accounts — each gets its own credentials directory, and run state is already per-project. Running two copies of the same subscription in parallel can still trigger OAuth refresh-token rotation that invalidates the other session, so use a distinct profile (account) per concurrent run. This caveat is documented in the README and CLAUDE.md.
Testing
tests/core/test_profiles.py(lifecycle, persistence, env resolution,CLAUDETM_PROFILEoverride precedence) andtests/core/test_credential_profiles.py(config-dir resolution, api-key short-circuit).tests/property/are unrelated and also fail onmain).ruff check,ruff format, andmypyclean on the changed files.Backwards compatibility
No profile configured → behavior is unchanged (default
~/.claude/.credentials.json). The existingCREDENTIALS_PATHpatch points used by tests still work.Summary by CodeRabbit
profilecommand withadd/list/use/show/remove/loginworkflows (with masked secret display).