> ./run_exploration.sh

@ahikmah/lab / research archive

Back to Research Log

Terminal illustration showing local profiles, health states, a transaction lock, and recovery flow

claudex-switch is a Node.js command-line tool for selecting which Codex OAuth Credential CLIProxyAPI makes active for Claudex.

Claudex sends its model requests through CLIProxyAPI. The proxy reads Codex authentication files from one active credential directory. claudex-switch manages that local boundary: it keeps inactive Credentials in named Profile directories and moves exactly one selected Credential into the directory read by the proxy.

It does not switch a Claude setting, rename a remote account, or revoke provider access. It switches the local Codex OAuth Credential that CLIProxyAPI uses on behalf of a Claudex session.

That distinction defines the whole project. A Profile is a named local slot. A Credential is the authentication data stored in that slot. A provider account is the remote identity behind the Credential. The profile name, not the email address, is the selector used by every command.

Why a file switch needs a lifecycle

The simplest implementation would copy a credential file into place and restart the proxy. That approach leaves difficult questions unanswered:

  • What happens to the previously active Credential?
  • What if two commands move the same files at the same time?
  • What if the process receives Ctrl-C between two moves?
  • What if local storage contains a symlink, duplicate file, or unexpected entry?
  • What if the Credential switch succeeds but CLIProxyAPI fails to restart?
  • How can a later command distinguish a failed operation from a half-completed one?

claudex-switch treats each mutation as a lifecycle with preparation, locking, a private transaction journal, proxy coordination, commit, rollback, and cleanup. Read-only inspection uses a separate path and does not create storage as a side effect.

Storage model

The default active credential directory is ~/.cli-proxy-api. The default Profile store is ~/.cli-proxy-api-accounts. Both can be redirected through environment variables when CLIProxyAPI uses another layout.

The invariant is intentionally strict:

  • The active directory contains at most one regular codex-*.json Credential.
  • An inactive Profile directory contains exactly one Credential.
  • The selected Profile name is recorded separately from the Credential file.
  • The active Profile’s Credential is in the active directory, not duplicated in its Profile directory.
  • Profile names are unique without case differences and cannot collide with command names.
  • Email is display data. Two Profiles may have the same email, but exact duplicate Credential data is rejected.

Directories must be private and Credentials, state, lock, and transaction files must use private file permissions. Symlinked storage, hidden or unexpected entries, multiple Credentials, missing Credentials, and case-conflicting Profile names are treated as unsafe state. Mutating commands stop instead of guessing which file is correct.

This model makes the selected Credential visible to CLIProxyAPI while retaining named inactive Profiles for later use.

One mutation protocol for every command

All state-changing commands share runMutation. The common sequence is:

Acquire the operation lock
    -> reject unresolved recovery state
    -> validate the complete storage layout
    -> check for a running Claudex or proxy session
    -> prepare and validate the command-specific change
    -> write a private transaction record
    -> stop CLIProxyAPI when it is running
    -> apply journaled file and state operations
    -> mark the transaction committed
    -> restore or start the proxy when required
    -> remove the transaction record and release the lock

The operation lock prevents two commands from moving the same Credentials concurrently. A session check warns when Claudex or CLIProxyAPI appears to be running. --force bypasses only this warning. It does not bypass name validation, storage checks, the operation lock, or pending recovery.

Every file move is written to the transaction record before and after it is applied. Directory creation, active-profile state changes, and permanent deletion have their own journal steps. The record contains operation metadata and paths, not Credential values.

If a step fails before commit, the tool reverses the journal in the opposite order. If rollback also fails, the transaction becomes recovery-required. Later mutations remain blocked so a second operation cannot hide or compound the first failure.

What each lifecycle command does

use NAME

use selects an existing Profile without running a new OAuth login. It validates the active state and the target directory first. If another Profile is active, its Credential moves back to its own directory. The target Credential then moves into the CLIProxyAPI auth directory, and the selected Profile record changes to the target name.

Selecting an already active Profile is a no-op for credential files, but the command can still ensure that the proxy is available. Duplicate storage for the active Profile is not accepted as a harmless no-op.

add NAME

add creates a new named Profile and makes it active. The current Credential, when present, first moves back to its current Profile directory. The tool then runs CLIProxyAPI with --codex-login and points CLIPROXY_AUTH_DIR at the controlled login directory.

After login, the command requires exactly one new codex-*.json file. It applies private permissions and validates the file before registering it. The generated Credential is recorded as transaction-owned so rollback can distinguish it from a pre-existing Credential. On success, the new Credential becomes active and the selected Profile changes to the new name.

reauth NAME

reauth replaces the Credential of an existing Profile while preserving whether that Profile was active or inactive.

The old Credential is validated and retained while the OAuth login runs. The new login must create exactly one valid Credential, and that Credential must not duplicate another Profile. If the provider email changes, the operation stops unless --allow-account-change was supplied explicitly.

For an active Profile, the new Credential replaces the file in the active directory. For an inactive Profile, the command temporarily makes space for login, installs the new Credential back into the inactive Profile directory, and restores the original active Credential. Only after the new Credential is placed successfully does the transaction permanently remove the old one.

This makes reauthentication a replacement of one Profile’s Credential, not an implicit profile switch.

deactivate

deactivate moves the current Credential from the active directory back into its Profile directory and removes the selected Profile record. The Profile remains ready for later use. CLIProxyAPI is left without a selected Codex Credential.

Deactivation is different from deletion. It removes active selection, not stored identity.

delete NAME

delete is limited to an inactive Profile. Interactive use requires the exact stored Profile name as confirmation; --yes exists for an already approved script. The command removes the selected local Credential and its Profile directory. It does not select a replacement and does not retain a hidden backup.

The command has no remote account operation. Deleting a Profile does not delete or revoke the provider account.

rename OLD_NAME NEW_NAME

rename changes the local Profile directory and updates the active Profile record when the renamed Profile is selected. It preserves the Credential, email, health state, and active or inactive status. Name rules and case-insensitive uniqueness are checked before the move.

CLIProxyAPI service coordination

The CLI can manage CLIProxyAPI through Homebrew services on macOS or a user-level systemd service on Linux. Service management can also be disabled for an externally managed proxy.

When the proxy is running, a mutation stops it before moving Credentials and starts it again afterward. use, add, and reauthentication of the active Profile also start a supported proxy that was stopped. Operations that do not create an active Profile, such as deactivate, delete, and rename, preserve a previously stopped service.

Service state is not merged with Credential state. If the profile transaction commits and the proxy then fails to start, the new Profile selection remains committed. The command returns a service error instead of rolling a valid Credential transition back. If a mutation itself fails, its file changes are rolled back before the prior service state is restored.

This separation is important for diagnosis. “The selected Credential changed” and “the proxy is available” are two results, not one Boolean success flag.

Cancellation, crash recovery, and repair

Graceful cancellation has a defined path. Ctrl-C and SIGTERM set a cancellation signal. The running mutation rolls back its recorded steps, releases the operation lock, and returns signal-specific exit codes.

A process crash or forced termination cannot run that cleanup. The private transaction record and any staged files remain as evidence of the incomplete operation. The next mutation sees the record and stops.

doctor --repair is the only automatic repair path. It can:

  • restore required private permissions on known directories and files;
  • remove an operation lock only after confirming that its recorded owner process is not running;
  • clear a complete committed or rolled-back record;
  • roll back a staged transaction only when the journal and current files define one safe result.

Repair preflights the whole rollback before changing state. It refuses to follow symlinks, remove unexpected data, trust paths outside the known storage roots, choose between ambiguous Credentials, or infer missing provenance. An ambiguous transaction remains visible and returns an unsafe-state result for manual handling.

This is a deliberate limit. Automatic recovery is useful only when the tool can prove which previous state it is restoring.

Inspection is separate from mutation

list, current, and the default doctor path inspect local state without contacting the provider. They report Profile names, display email, active state, health status, and stable issue codes. They do not expose Credential or token values.

The local health model distinguishes:

  • active: the selected Profile has one valid local Credential;
  • ready: an inactive Profile has one valid local Credential;
  • invalid: local Credential or storage state is missing, malformed, duplicated, or unsafe;
  • unregistered: the active directory has a Credential with no matching Profile;
  • needs-reauth: an explicit online check reports that the Credential is rejected or expired;
  • unknown: the online check cannot determine validity because of provider or network uncertainty.

doctor --online is the only read path that contacts the provider. It checks each locally valid Credential against the Codex models endpoint. HTTP 401 or 403 becomes needs-reauth; network errors and unclassified responses become unknown. The check does not start login, change Profile files, select another Profile, or restart the proxy.

Human-readable output and --json expose the same inspection model. JSON uses schemaVersion: 1, stable issue codes, and no secrets. Exit codes separate success, ordinary operation or service errors, invalid command input, unsafe or incomplete local state, and signal cancellation.

Failure modes that changed the design

The code and its tests cover failure paths that are easy to miss in a file-moving utility:

  • Two mutations competing for the same store are rejected by the lock.
  • A new login that produces zero, multiple, malformed, public, or duplicate Credentials is rejected.
  • Reauthentication does not delete the old Credential before the replacement passes validation.
  • A failed move restores previously moved Credentials when rollback succeeds.
  • A failed rollback preserves its transaction record and blocks later mutation.
  • A stale lock is removable only after its owner is known to have stopped.
  • Symlinked Profile roots and Credentials are reported without modifying their targets.
  • Unexpected files and ambiguous storage are left in place.
  • A proxy restart failure does not falsify the already committed Profile state.
  • --force cannot turn an unsafe store into an accepted one.

The common pattern is fail-closed mutation and read-only diagnosis. The tool prefers an explicit unresolved state over a convenient guess that could destroy the only valid Credential copy.

Evidence in the repository

The main implementation is split between the CLI adapter and the lifecycle module:

  • bin/claudex-switch.js resolves storage paths, discovers Homebrew or systemd service management, runs CLIProxyAPI login, checks for active sessions, and implements the optional online health adapter.
  • lib/profile-lifecycle.js owns terminology, storage validation, inspection reports, command behavior, locks, transaction journaling, rollback, deterministic repair, proxy-state policy, the versioned JSON output contract, and exit codes.
  • Eight ADRs record the decisions around local-only profiles, identity, concurrency, recovery, service state, email handling, profile names, private artifacts, and refusal of ambiguous storage.
  • The domain document defines Profile, Credential, provider account, active, deactivated, reauthentication, and deletion as separate concepts.

The automated Node test suite currently passes all 119 tests. It covers read-only behavior, malformed and symlinked storage, permissions, online status mapping, stale and active locks, deterministic and ambiguous recovery, each lifecycle command, duplicate Credentials, identity changes, concurrent mutation, signal cancellation, rollback, proxy failures, JSON contracts, and exit codes.

That test result is evidence for the local state machine under its test adapters. It is not evidence that every provider response, service-manager installation, or interrupted real OAuth session behaves identically on every machine.

Current limits

claudex-switch requires Node.js 18 or newer and a local CLIProxyAPI installation. Service discovery supports Homebrew on macOS and user-level systemd on Linux; other process managers must be handled outside the tool.

The default workflow is offline. Only doctor --online checks provider acceptance, and an unknown result cannot establish whether a Credential is usable. The tool does not measure quota or choose an account based on remaining capacity.

Profiles and transaction state are local to one machine. There is no cross-machine synchronization, shared store, or remote account administration. Recovery is automatic only when one rollback result can be derived from recorded evidence. Ambiguous or unexpected storage remains a manual problem by design.

The useful result is not a shorter login command. It is a defined lifecycle for the Credential that Claudex reaches through CLIProxyAPI: explicit selection, validated local storage, serialized mutation, recoverable file moves, independent proxy state, and diagnosis that can remain read-only.