Skip to content

feat(profiles): named auth profiles for multi-account isolation#102

Merged
OGtwelve merged 3 commits into
mainfrom
feat/profiles
Jun 29, 2026
Merged

feat(profiles): named auth profiles for multi-account isolation#102
OGtwelve merged 3 commits into
mainfrom
feat/profiles

Conversation

@OGtwelve

@OGtwelve OGtwelve commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

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 isolated CLAUDE_CONFIG_DIR per profile under ~/.claudetm/profiles/<name>/. The bundled claude CLI reads and refreshes credentials inside that directory, so two oauth profiles never clobber each other.
  • api-key — a direct API key + base URL injected as ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL (e.g. z.ai / GLM via an Anthropic-compatible endpoint).

Implementation

  • core/profiles.pyProfileManager (registry at ~/.claudetm/profiles.json, base dir overridable via CLAUDETM_HOME), the Profile model, and resolve_runtime_env() which maps the active profile (or a per-run CLAUDETM_PROFILE override) 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.
  • Env injection — the resolved env is merged in at the SDK subprocess boundary in core/agent_query.py (single-turn queries) and core/conversation.py (multi-turn). No-op when no profile is active.
  • core/credentials.pyCredentialManager now resolves the active oauth profile's config directory. The credentials path is an instance property; the patchable CREDENTIALS_PATH class default is preserved for backwards compatibility. For api-key profiles the OAuth-file pre-flight is short-circuited (those authenticate via env).
  • CLIclaudetm profile add/list/use/show/remove/login. login launches claude with the profile's CLAUDE_CONFIG_DIR so you can /login into an isolated session.

Usage

# oauth profile (a Claude subscription)
claudetm profile add work
claudetm profile login work          # run /login inside, then exit

# api-key profile (e.g. z.ai)
claudetm profile add zai --type api-key \
    --api-key sk-... --base-url https://api.z.ai/api/anthropic

claudetm profile use work            # set active
claudetm profile list                # active marked with →

# parallel runs under different accounts (two checkouts)
CLAUDETM_PROFILE=accountA claudetm start "..."   # in repo-a/
CLAUDETM_PROFILE=accountB claudetm start "..."   # in repo-b/

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

  • New unit tests: tests/core/test_profiles.py (lifecycle, persistence, env resolution, CLAUDETM_PROFILE override precedence) and tests/core/test_credential_profiles.py (config-dir resolution, api-key short-circuit).
  • Full suite green (the two pre-existing flaky Hypothesis property tests in tests/property/ are unrelated and also fail on main).
  • ruff check, ruff format, and mypy clean on the changed files.
  • Manual CLI smoke test of add/list/use/show with secret masking.

Backwards compatibility

No profile configured → behavior is unchanged (default ~/.claude/.credentials.json). The existing CREDENTIALS_PATH patch points used by tests still work.

Summary by CodeRabbit

  • New Features
    • Added CLI-driven authentication profiles for multiple accounts and custom endpoints.
    • Supports both OAuth-based profiles (with isolated config) and API-key profiles.
    • Enables per-run profile selection via environment override and applies profile auth to both single-turn and multi-turn runs.
    • Added a profile command with add/list/use/show/remove/login workflows (with masked secret display).
  • Bug Fixes
    • Credentials are now loaded from the active profile’s isolated location when applicable; API-key profiles authenticate without relying on OAuth credential files.
  • Documentation
    • Updated the README and CLI documentation with profile storage/selection details and parallel-isolation guidance.

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.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 46cb2a12-c2a9-4518-9983-f6cca1d111b3

📥 Commits

Reviewing files that changed from the base of the PR and between 23d6170 and 3189455.

📒 Files selected for processing (5)
  • README.md
  • src/claude_task_master/cli_commands/profile.py
  • src/claude_task_master/core/credentials.py
  • src/claude_task_master/core/profiles.py
  • tests/core/test_profiles.py
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/claude_task_master/cli_commands/profile.py
  • src/claude_task_master/core/credentials.py

📝 Walkthrough

Walkthrough

Adds named oauth and api-key profiles, registry-backed profile management, profile-aware credential lookup, runtime env injection into subprocesses, and a new claudetm profile command group. Tests and docs were updated for the profile flow.

Changes

Multi-account Profile Management

Layer / File(s) Summary
Profile data models and registry
src/claude_task_master/core/profiles.py
Defines profile types, exceptions, models, environment mapping, active profile resolution, base directory selection, validation, and registry persistence.
Profile-aware CredentialManager
src/claude_task_master/core/credentials.py
Adds per-instance credential path resolution, active-profile lookup, and api-key short-circuits for token loading and verification.
Runtime env injection into subprocesses
src/claude_task_master/core/agent_query.py, src/claude_task_master/core/conversation.py
Extends query execution and conversation option creation to merge resolved profile env into the Claude subprocess configuration.
CLI profile subcommands
src/claude_task_master/cli_commands/profile.py, src/claude_task_master/cli.py
Adds the claudetm profile Typer group with add, list, use, show, remove, and login commands, plus registration on the main CLI app.
Tests and documentation
tests/core/test_profiles.py, tests/core/test_credential_profiles.py, README.md, CLAUDE.md
Adds profile registry, env resolution, and credential profile tests, and updates README.md and CLAUDE.md with the profiles guide.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

claudetm

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.23% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding named profiles for isolated authentication across accounts and endpoints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/profiles

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

markdownlint-cli2 wrapper config was not available before execution


Comment @coderabbitai help to get the list of available commands.

CI runs mypy over the whole tree; assert config_dir is not None before
constructing a Path from it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a981034 and 25449b4.

📒 Files selected for processing (10)
  • CLAUDE.md
  • README.md
  • src/claude_task_master/cli.py
  • src/claude_task_master/cli_commands/profile.py
  • src/claude_task_master/core/agent_query.py
  • src/claude_task_master/core/conversation.py
  • src/claude_task_master/core/credentials.py
  • src/claude_task_master/core/profiles.py
  • tests/core/test_credential_profiles.py
  • tests/core/test_profiles.py

Comment thread src/claude_task_master/cli_commands/profile.py Outdated
Comment thread src/claude_task_master/core/credentials.py
Comment thread src/claude_task_master/core/profiles.py Outdated
Comment thread src/claude_task_master/core/profiles.py
Comment thread src/claude_task_master/core/profiles.py
Comment thread tests/core/test_profiles.py Outdated
…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.
@OGtwelve

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — all 6 findings addressed in 3189455:

  1. api-key via CLI flag (profile.py) — removed the --api-key flag. The key is now read from CLAUDETM_API_KEY or a hidden prompt, so it never lands in shell history / process listings. Examples updated.
  2. Silent ~/.claude fallback on resolution failure (credentials.py) — dropped the catch-all. The active profile is resolved without masking; a missing/corrupt selection now propagates instead of green-lighting the wrong account.
  3. Masking failures with empty env (profiles.py resolve_runtime_env) — returns {} only when no profile is selected; an explicitly selected-but-missing profile (or corrupt registry) fails fast via ProfileNotFoundError.
  4. World-readable credential storage (profiles.py save / oauth dir)profiles.json and oauth profile homes are now created owner-only (0o600 / 0o700) with explicit chmod, not umask-dependent.
  5. Path traversal in profile name (profiles.py add) — names are validated against a safe charset (rejecting separators / dot segments / absolute paths), with a resolved-path guard as defense in depth.
  6. Test asserted unsafe behavior (test_profiles.py) — updated to assert fail-fast on an unknown override, plus new tests for name validation and owner-only permissions.

ruff, mypy, and the profile/credential suites are green.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

@OGtwelve Thanks — this looks like a solid follow-through.

Good fixes across the board, especially:

  • removing the API key CLI flag in favor of env/hidden prompt handling,
  • switching profile resolution to fail fast instead of silently falling back,
  • tightening file/directory permissions explicitly rather than relying on umask,
  • and adding tests for the security-sensitive edge cases.

Appreciate the clear breakdown and the green ruff / mypy / profile+credential suite signal. I’m marking my review comments as resolved.

🐇✨


✏️ Learnings added
Learnt from: OGtwelve
URL: https://github.com/developerz-ai/claude-task-master/pull/102

Timestamp: 2026-06-29T06:47:11.743Z
Learning: In `src/claude_task_master/cli_commands/profile.py`, API-key profile creation should not accept the secret via a CLI flag. The API key is sourced from `CLAUDETM_API_KEY` or a hidden prompt so it does not appear in shell history or process listings.

Learnt from: OGtwelve
URL: https://github.com/developerz-ai/claude-task-master/pull/102

Timestamp: 2026-06-29T06:47:11.743Z
Learning: In `src/claude_task_master/core/profiles.py` and `src/claude_task_master/core/credentials.py`, explicit active-profile selection must fail fast when the selected profile is missing or the registry is corrupt. `resolve_runtime_env()` should return an empty environment override only when no profile is selected, not when resolution fails.

Learnt from: OGtwelve
URL: https://github.com/developerz-ai/claude-task-master/pull/102

Timestamp: 2026-06-29T06:47:11.743Z
Learning: In `src/claude_task_master/core/profiles.py`, profile storage is security-sensitive: `profiles.json` must be owner-only (`0o600`), OAuth profile directories must be owner-only (`0o700`), and profile names must be validated against a safe charset with path-traversal defenses including a resolved-path guard.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Comments resolved and changes approved.

@OGtwelve OGtwelve merged commit dab8272 into main Jun 29, 2026
8 checks passed
@OGtwelve OGtwelve deleted the feat/profiles branch June 29, 2026 06:52
@OGtwelve OGtwelve mentioned this pull request Jun 29, 2026
OGtwelve added a commit that referenced this pull request Jun 29, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant