mat multi-account-tool

AI CLI 계정 스위처

multi-account-tool (mat)

하나의 TUI에서 여러 AI CLI 계정(Claude Code, Codex, Gemini CLI, Aider, Kimi, Qwen, Crush, OpenCode, Goose, Grok Build)을 전환한다. 계정마다 프로필을 하나씩 저장해 두고, 매번 logoutlogin을 반복하는 대신 키 한 번으로 바꾼다.

mat은 보수적으로 동작한다. macOS Keychain 항목을 백업하고, 부분 실패는 롤백하며, 파일은 원자적으로 쓴다. 평문 자격증명 백업 위험을 분명히 알리고, swap 전 자격증명 freshness(OAuth refresh-token rotation 포함)도 점검한다. 라이브 자격증명이 저장된 프로필과 불일치한 경우 실제 swap 전에 재캡처 / 폐기 / 취소 중 하나를 선택하게 한다.

╭ Multi-Account Tool ────────────────────────────────╮
│  AI CLI 계정 스위처                                 │
╰─────────────────────────────────────────────────────╯

  > Claude Code            [활성: personal] ✓
    Codex CLI              [활성: work]     ✓
    Gemini CLI              [활성: personal] ✓

왜 만들었나

어떻게 동작하나

mat은 각 CLI의 자격증명만 swap한다. hooks, agents, CLAUDE.md, 대화 이력, 설정 같은 나머지는 그대로 둔다.

CLI 자격증명 위치 전환 방식
Claude Code macOS Keychain (Claude Code-credentials) Keychain 항목 swap
Codex CLI ~/.codex/auth.json 파일 swap
Gemini CLI ~/.gemini/oauth_creds.json, google_accounts.json 파일 swap
Aider ~/.aider.conf.yml 파일 swap
Kimi CLI ~/.kimi/config.toml 파일 swap
Qwen Code CLI ~/.qwen/settings.json, ~/.qwen/.env 파일 swap
Crush ~/.config/crush/crush.json, ~/.local/share/crush/crush.json 파일 swap
OpenCode ~/.local/share/opencode/auth.json (OS 공통, XDG 표준) 파일 swap
Goose 기존 Keychain/Secret Service + YAML과 ~/.config/goose/providers/의 고정 파일 5개/제한 디렉터리 2개 프로필 swap 전용; v1.43 고정 경로만, discovery/session 격리 없음
Grok Build ~/.grok/auth.json 파일 swap (profile-swap-only)

OAuth Rotation 안전성 매트릭스

일부 CLI는 OAuth refresh-token rotation(RFC 6749 권장 방식)을 사용한다. refresh token이 사실상 1회용이라, 다음 refresh 성공 후 provider가 이전 token을 무효화할 수 있다. 이때 mat이 오래된 snapshot을 복원하면 provider가 token을 "already used"로 거부하고 사용자는 다시 로그인해야 한다. 아래 표는 mat이 지원하는 CLI별 위험도와 안전 워크플로를 정리한 것이다.

CLI 인증 방식 rotation 위험 mat 안전 워크플로
Codex CLI OAuth (tokens.refresh_token, tokens.account_id) 🔴 높음 — token revoke 재현됨 swap 전 mat freshness codex 점검 / 일회성 명령은 mat exec
Gemini CLI OAuth (refresh_token + google_accounts.json.active) 🔴 높음 Codex와 동일
OpenCode provider별 OAuth (provider.refresh, provider.accountId) 🔴 높음 Codex와 동일
Claude Code macOS Keychain (Anthropic OAuth) 🟢 완화됨 — identity-aware adapter (subscriptionType + macOS keychain account) mat exec 또는 mat freshness claude (high-confidence rotation 분류)
Goose 기존 backend/YAML + v1.43 고정 provider cache 7개 ⚠️ provider cache diff는 opaque low-confidence rotation; 기존 YAML/keyring은 identity-aware 유지 mat freshness goose가 고정 source별 결과 보고
Crush Hyper/Copilot OAuth(providers.*.oauth) + mirrored/정적 API key ⚠️ 저장된 계정 identity 없음 — adapter 기반 conservative low-confidence byte-diff만(확인된 rotation 아님) swap 전 mat freshness crush; project/CRUSH_*/XDG/provider-env가 전역 swap을 우회할 수 있음(플랫폼 지원 참고)
Grok Build Browser/OIDC ~/.grok/auth.json ⚠️ 미확인 — identity adapter 없이 fallback byte-diff만 사용 TUI 프로필 전환만 사용(grok 선택 후 대상 프로필 선택); 선택한 프로필을 신뢰하기 전 config/env/project override를 검토
Aider / Kimi / Qwen 정적 API key 🟢 없음 일반 swap으로 충분 — 단 환경변수 / project-local 설정이 mat의 swap을 우회할 수 있음 (아래 "플랫폼 지원" 참고)

mat freshness [<cli>] [--profile <name>] [--json] 명령으로 swap 전 라이브와 활성 프로필의 자격증명을 비교한다. exit code 0 = 안전: 모든 source가 fresh이거나 adapter가 high/medium confidence로 identity 유지 rotated를 확인한 경우다. exit code 1 = swap 전 조치가 필요한 상태: stale, fallback/byte-diff 기반 low-confidence rotated, inflight, 또는 프로필/source 부재를 뜻한다. 장기 실행 세션에는 mat exec를 권장한다. 명령 종료 후 자동으로 이전 프로필을 복원하지만, mat 자체가 SIGKILL을 받으면 복원이 일어나지 않는다(보안 섹션 참고).

OAuth rotation 대응: TUI의 swap 흐름은 swap 직전 라이브 freshness를 점검하고, 차이를 감지하면 재캡처 / 폐기 / 취소 3옵션 dialog를 표시한다. 재캡처는 라이브 자격증명을 snapshotLiveToProfile로 활성 프로필에 저장한 뒤 swap하고, 폐기는 자동 snapshot을 건너뛰고 swap한다(데이터 손실). 취소는 swap을 실행하지 않는다. mat exec는 종료 시 라이브 자격증명을 swap-target 프로필로 재캡처한 뒤 원래 활성 프로필로 복원하므로, 명령 실행 중 발생한 rotation도 보존된다 — SIGINT/SIGTERM/SIGHUP까지 보호한다. SIGKILL은 OS 보장상 trap이 불가능하므로 다음 mat 호출의 stale-recovery가 사용자에게 안내한다. Claude/Goose identity-aware adapter는 high/medium confidence로 rotation과 다른 계정을 구분해, 안전한 swap에서 [low conf] dialog noise를 제거한다.

플랫폼 지원

CLI macOS Linux Windows Overrides / 알려진 제약
Claude Code macOS는 Keychain, Linux는 ~/.claude/.credentials.json. mat session은 Linux에서 CLAUDE_CONFIG_DIR로 지원; macOS Keychain은 세션 격리 불가
Codex CLI ⚠️ 미검증 ~/.codex/auth.json (cross-platform file path)
Gemini CLI ⚠️ 미검증 ~/.gemini/oauth_creds.json + google_accounts.json; mat sessionGEMINI_CLI_HOME + .gemini envSubdir 사용. Google은 2026-06-18 개인/무료 tier의 CLI 접근을 Antigravity 쪽으로 전환했지만 enterprise/Cloud/API-key 경로는 별개이며, 이 가용성 변화가 mat의 자격증명 경계를 바꾸지는 않는다.
Google Antigravity (agy) ❌ blocked ❌ blocked ❌ blocked 1.1.2에서 재검증했다. Gemini CLI 자격증명 source가 아니다. 공개 문서는 system keyring 인증 + Google Sign-In fallback만 설명하며, 안정적인 keyring service/account, token profile, credential redirect, recapture 계약은 공개하지 않는다. ~/.gemini/antigravity-cli/의 설정/cache와 관찰 가능한 antigravity-oauth-token 파일만으로는 안전 지원 근거가 부족하다. auth-store research note 참고.
Aider ⚠️ 미검증 mat session start는 계속 미지원(credential-dir env 없음). mat session run aider는 partial support: mat--config <session>/command/aider.yml + --env-file <session>/command/.env를 강제하고 알려진 argv/env/dotenv/OAuth-key/model-sidecar/provider-chain 우회를 hard-stop
Kimi CLI ⚠️ 미검증 env override: MOONSHOT_API_KEY 등이 ~/.kimi/config.toml을 우회
Qwen Code CLI ⚠️ 미검증 v0.19.10에서 재검증했다. profile swap과 mat session start qwenQWEN_HOME을 재지정하지만 advisory 범위다. Qwen은 shell/project/ancestor/home 설정과 custom modelProviders[].envKey source를 계속 사용할 수 있다. 완전한 auth/source 계약을 fail-closed할 수 있을 때까지 mat session run qwen은 의도적으로 미지원이다.
Crush ⚠️ 미검증 Hyper/Copilot 로그인 시 providers.* 아래 OAuth와 mirrored api_key가 공존할 수 있음(pin charmbracelet/crush@7b24cc09…, 조회 2026-07-15 KST). 저장된 계정 identity가 없어 freshness diff는 low-confidence attention-required. project-local / env override: ./.crush.json / ./crush.json, CRUSH_GLOBAL_*, XDG root, provider API-key env가 swap된 전역 파일을 우회할 수 있음
OpenCode ⚠️ 미검증 canonical anomalyco/opencode v1.18.1에서 재검증했다. OS 공통 XDG 경로($XDG_DATA_HOME/opencode/auth.json, 기본 ~/.local/share/opencode/auth.json). mat session start는 broad XDG_DATA_HOME 기반 EXPERIMENTAL; mat session run opencode는 command-scoped로 알려진 local env/config 우회를 hard-stop
Goose ✅ os-keyring 기존 goose/secrets backend + YAML 동작은 유지한다. Linux 기본 keyring 경로에는 secret-tool(libsecret-tools)과 실행 중인 keyring daemon이 필요하다. 도구 부재/접근 거부는 stale YAML fallback이 아니라 명시 에러다. CLI 부재만으로 Goose의 libsecret keyring 미사용이 증명되지 않기 때문이다. Goose가 file backend를 쓰도록 의도적으로 설정한 경우에만 GOOSE_DISABLE_KEYRING=1을 사용한다(값 무관 presence-only). MAT는 Goose v1.43.0 source commit 5a9eb7e로 확인한 providers/ 하위 파일 5개와 제한된 디렉터리 2개만 swap한다. 일곱 번째 artifact는 Hugging Face oauth/tokens.json이며, 이를 우회할 수 있는 HF_TOKEN은 ambient warning으로 보고한다. discovery/session/run은 열지 않고 schema는 redacted fixture 승인 전 opaque이며 Windows는 미지원이다.
Grok Build ⚠️ 미검증 현재 지원은 기본 signed-in browser/OIDC 토큰 파일 ~/.grok/auth.jsongrok-auth.json으로 swap한다. mat session start/run grok은 미지원이다. ~/.grok/config.toml의 model api_key/env_key, XAI_API_KEY, GROK_* auth/model env, GROK_HOME, project .grok/config.toml, MCP credentials는 auth.json을 override/우회할 수 있으므로 선택한 프로필을 신뢰하기 전에 unset/검토해야 한다.

"⚠️ 미검증" = swap 로직은 platform-agnostic file I/O라 동작 가능성이 있지만, 본 프로젝트 CI는 macOS + Ubuntu만 검증한다. Windows 경로는 각 CLI의 공식 문서 기반 추정이며 실제 실행은 검증하지 않았다. patch / 버그 리포트 환영.

CLI 하나의 정확한 지원 경계를 보려면 mat support <cli>(또는 mat explain <cli>)를 실행한다. 현재 swap, freshness, session 지원 상태와 caveat, ambient override 위험, 마지막으로 확인한 upstream 가정을 함께 출력한다.

foreground 프로필 전환이나 mat exec 중 provider API-key env var, project-local config 파일 같은 high-confidence ambient 우회 채널이 보이면 mat은 경고를 출력한다. 이 경고는 정보성이다. 아직 차단하거나 scrub하지는 않는다. 의도한 override라면 계속 진행하면 되고, 아니라면 선택한 프로필을 신뢰하기 전에 표시된 env/config source를 unset 하거나 제거하라.

왜 Grok session isolation은 아직 켜지 않았나

Grok Build 지원은 현재 의도적으로 profile-swap-only다. xAI 공개 Build 문서(Getting Started, Enterprise Deployments)는 여러 자격증명·config·계정 선택 채널을 설명한다: browser OIDC/device auth, external auth-provider command, XAI_API_KEY 직접 API-key 인증, ~/.grok/config.toml의 model-level api_key / env_key, managed/requirements config layer, project-visible instructions/plugins/hooks/MCP server, 그리고 이 모든 discovery view를 보여주는 grok inspect. 즉 ~/.grok/auth.json은 여러 채널 중 하나일 뿐이므로, 그 파일만 세션 디렉토리로 복사하거나 redirect해도 mat session 자식이 선택한 profile만 사용한다고 증명할 수 없다.

향후 mat session run grok은 별도 설계가 필요하다. 가능한 방향은 (1) API-key-only 경계를 만들고 browser/OIDC, config, env, project, plugin, hook, MCP override 채널을 hard-stop하거나, (2) upstream이 명확한 recapture semantics를 가진 Grok credential/config-root redirect를 제공하는 것이다. 그 전까지는 TUI에서 Grok 프로필을 전환하고(grok 선택 후 대상 프로필 선택), 활성 profile을 신뢰하기 전에 위 override source를 unset/검토하라.

전환 흐름 (데이터 손실 없음)

  1. swap 전 freshness 점검 — 라이브 자격증명이 활성 프로필 저장본과 drift(OAuth refresh 토큰 회전 등)된 상태면, 아래 1~3 단계 전에 재캡처 / 폐기 / 취소 dialog가 먼저 표시된다. CLI별 분류 신뢰도는 위의 "OAuth Rotation 안전성 매트릭스" 참고.
  2. 현재 라이브 자격증명을 현재 활성 프로필에 자동 스냅샷
  3. 선택한 프로필의 저장된 자격증명을 라이브 위치로 원자적으로 복원
  4. 활성 프로필 포인터 업데이트

multi-source CLI(예: Gemini의 두 파일)에는 부분 실패 롤백도 적용된다. 한 source 복원에 실패하면 이미 복원된 source를 라이브 백업으로 되돌려, 라이브가 절반만 새 프로필인 상태를 막는다.


설치

Homebrew (macOS 권장)

brew tap ictechgy/mat
brew install mat

npm

npm install -g multi-account-tool

소스에서 빌드

git clone https://github.com/ictechgy/multi-account-tool.git
cd multi-account-tool
npm install
npm run build
npm link

설치 확인

mat --version                  # 설치된 semver 출력
mat --help                     # subcommand 목록 (TUI 옵션 + `mat exec` / `mat session` / `mat plugin` / `mat freshness` / `mat doctor`)
node scripts/smoke-test.mjs    # 소스 체크아웃 전용 — read-only smoke test (CLI 정의 로드 + path resolve 확인, 자격증명 미수정)

smoke test는 read-only라 활성 mat 프로필이 있는 환경에서도 안전하다.


사용

mat              # TUI 실행
mat --version    # 설치된 버전 출력
mat --help       # 짧은 사용법 (subcommand: exec / session / plugin / freshness / doctor)

TUI가 열리면 CLI 선택 → 프로필 선택 → 전환 순서로 진행한다.

첫 실행

이미 로그인된 자격증명이 감지되면 default 프로필로 가져올지 묻는다. 한 번 답하면 다음 실행부터 자동으로 다시 묻지 않는다 (수동 캡처는 언제나 가능).

새 계정 추가하기

  1. mat → CLI 선택 → a (새 프로필) → 이름 입력 (예: work)
  2. 새 프로필에서 Enter를 눌러 활성화한다. 라이브 자격증명이 활성 프로필의 저장본과 drift(OAuth refresh 토큰 회전 등)된 상태라면 swap 직전에 재캡처 / 폐기 / 취소 dialog가 표시된다 — 위의 전환 흐름 + OAuth Rotation 안전성 매트릭스 참고.
  3. 별도 터미널에서 해당 CLI의 로그인 명령 실행 (claude, codex, gemini 등). 라이브 자격증명이 새 계정 것으로 덮어쓰인다.
  4. mat으로 돌아와 같은 프로필에서 c(캡처)를 누른다. 새 라이브 자격증명이 프로필에 저장된다.
  5. 이후로는 Enter만으로 프로필 사이를 자유롭게 전환한다.

키바인딩

화면 동작
어디서나 q / Ctrl+C 종료
어디서나 Esc 뒤로
홈 / 프로필 ↑ ↓ 이동
홈 / 프로필 Enter 선택 / 전환
프로필 a 새 프로필 추가
프로필 c 포커스된 프로필에 현재 라이브 자격증명 캡처
프로필 r 이름 변경
프로필 d 삭제
Freshness dialog r / Enter 재캡처 (swap 전에 라이브를 활성 프로필에 저장)
Freshness dialog d 폐기 (자동 snapshot 건너뜀 — 데이터 손실)
Freshness dialog c / Esc swap 취소

mat exec — 한 명령에 한해 프로필 swap

mat exec <cli> <profile> -- <cmd...>

<profile>로 일시 swap → <cmd> 실행 → 명령 종료 시 원래 활성 프로필로 자동 복원.

# 한 번의 Claude 세션만 work 프로필로 실행, 종료 후 personal로 복원
mat exec claude work -- claude

# lterm과 조합 (선택 — `npm install -g @ictechgy/lterm`으로 별도 설치 필요)
lterm send-keys "mat exec claude work -- claude" Enter

동작:

이는 시간 격리이지 세션 격리가 아니다. 자식이 실행되는 동안 OS 전역 자격증명은 <profile>의 것이다. 두 터미널에서 서로 다른 mat exec를 동시에 띄우면 lock으로 직렬화된다. 터미널별로 서로 다른 계정을 동시에 써야 한다면 mat session을 사용하라.

종료 코드:

코드 의미
0 자식 정상 종료 0 (원복 성공)
2 사용 오류 (UsageError — spawn 전 검증 실패)
74 mat 자체의 복원 실패 (restoreError) — 자식 결과는 stdout/stderr로 출력됨
75 다른 mat exec가 CLI lock 보유 중 (LockHeldError — spawn 전)
128+N 자식이 시그널 N으로 종료(예: SIGINT면 130)
1 자식이 종료 코드 1로 끝났거나, mat 자체가 spawn 전후로 예상치 못한 에러 발생
그 외 (예: 3, 42) 자식의 non-zero 종료 코드를 그대로 전달

참고: 2 / 74 / 75mat 자체의 에러 모델로 예약되어 있다(spawn 전 검증 / lock 경합 / spawn 후 복원 실패). 그 외의 128 미만 non-zero 코드는 모두 자식의 종료 코드를 투명하게 전달한다. 74mat의 복원 실패인지 자식의 exit 74인지 헷갈리면 stderr의 restoreError 로그를 확인한다.

mat session — 세션별 격리 (터미널마다 다른 계정, 동시에)

mat session start <cli> <profile>   # <profile>로 격리된 subshell 실행
mat session run <cli> <profile> -- [cli-args...]
                                  # builtin CLI executable을 격리 env로 직접 실행
mat session run <cli> <profile> --check|--explain [--json] -- [cli-args...]
                                  # spawn 없이 정확한 session-run validator 사전 점검
mat session list [--json]           # 실행 중 / orphan 세션 목록
mat session stop <id>               # 세션 종료 또는 orphan 정리
mat status [--json]                 # dashboard/statusline용 active profile + session 요약

mat exec(lock으로 직렬화되는 시간 격리)와 달리 mat session진짜 동시 격리를 제공한다. 두 터미널이 같은 CLI의 다른 계정을 동시에 쓸 수 있다:

# 터미널 A
mat session start codex work        # CODEX_HOME이 격리 디렉토리 → "work" 계정

# 터미널 B (동시)
mat session start codex personal    # 독립 격리 디렉토리 → "personal" 계정

메커니즘 — env 주입 + copy-isolation. mat session start$SHELL을 실행하면서 CLI의 config-directory env(예: CODEX_HOME)가 ~/.multi-account-tool/sessions/<id>/ 아래 새 세션 전용 디렉토리를 가리키게 한다. mat은 선택한 프로필의 자격증명을 그곳에 0600 권한으로 복사하므로, subshell 안의 CLI는 격리된 계정만 읽는다.

작은 non-credential allow-list는 세션 로컬 스냅샷으로 함께 복사될 수 있다. Codex의 경우 config.tomlskills/가 격리된 CODEX_HOME으로 복사되어, live ~/.codex tree를 공유하지 않고도 사용자 skill을 사용할 수 있다. 종료 시 mat은 (OAuth rotation 등으로) 바뀐 자격증명만 프로필로 재캡처한 뒤 세션 디렉토리를 삭제한다. OS 전역 자격증명과 mat exec lock은 건드리지 않으므로 세션은 서로 간섭하지 않고 동시에 실행된다.

mat session run은 같은 materialize → env 주입 → 재캡처 → cleanup lifecycle을 쓰지만 shell을 열지 않는다. mat<cli>에 대응하는 builtin executable(예: codex)을 선택하고 [cli-args...]를 직접 넘긴다. -- 뒤는 임의 shell 명령이 아니라 선택된 CLI의 argv다. 현재 이 command-scoped 경계는 안전한 run path가 확인된 builtin(Codex, Kimi, Crush, Gemini CLI, Linux Claude, OpenCode safer-run, Aider partial-run)에만 열려 있다. Qwen은 의도적으로 제외된다.

실행 전에는 mat session run <cli> <profile> --check -- [cli-args...](또는 --explain)으로 동일한 지원 여부, profile, executable, Aider, OpenCode hard-stop validator를 점검할 수 있다. 이 경로는 CLI를 spawn하지 않고 session directory도 만들지 않는다. 실제 실행 preflight를 통과하면 exit 0, validation blocker가 있으면 1, 사용법/parser 오류는 2다. 자동화가 필요하면 그 --check/--explain 명령에 --json을 붙여 blocker, phase, 선택 executable, profile 존재 여부, 정확한 argv를 포함한 report를 받는다.

dashboard/statusline 용도로 mat status --json은 active profile과 session 요약을 담은 안정 schema-v1을 출력한다. active profile에는 capture 시점에 저장된 masked account/email fingerprint나 allowlisted tier/provider-mode 같은 identity metadata가 포함될 수 있지만, status는 credential file이나 keyring entry를 즉석에서 파싱하지 않는다. mat session list --json은 owner/child 상태와 root env 이름만 포함한 schema-v1 session lifecycle entry(active / orphan / unknown)를 출력하며, session root 절대 경로는 내보내지 않는다. 변이를 수행하는 session lifecycle 명령은 ~/.multi-account-tool/audit.jsonl에 best-effort redacted JSONL event를 append한다. persistent audit entry는 profile/session identifier를 hash 처리하고 secret-like string을 redact한다.

Prompt/statusline snippets

프롬프트 렌더러는 mat status --json을 사용할 수 있지만, 매 redraw 때 uncached로 실행하지 말아야 한다. status report는 session liveness를 검사할 수 있다. 아래 예시는 2초 동안 cache하고, mat 실행이나 JSON parsing이 실패하면 빈 출력으로 끝나며, 표시 전용이다. mat status --json만 호출하고 ~/.multi-account-tool, credential file, keyring, mat freshness, mat doctor를 직접 읽거나 실행하지 않는다.

공유 helper를 ~/.config/mat/statusline.zsh 같은 파일에 둔다:

: ${MAT_STATUS_CACHE_TTL:=2}
: ${MAT_STATUS_CACHE:="${XDG_CACHE_HOME:-$HOME/.cache}/mat/status.json"}

mat_status_cached() {
  local now mtime cache_dir tmp
  cache_dir="$(dirname "$MAT_STATUS_CACHE")" || return 0
  mkdir -p "$cache_dir" 2>/dev/null || return 0

  now=$(date +%s)
  if [[ -r "$MAT_STATUS_CACHE" ]]; then
    mtime=$(stat -f %m "$MAT_STATUS_CACHE" 2>/dev/null)
    if [[ -z "$mtime" || "$mtime" == *[!0-9]* ]]; then
      mtime=$(stat -c %Y "$MAT_STATUS_CACHE" 2>/dev/null || echo 0)
    fi
    if [[ -n "$mtime" && "$mtime" != *[!0-9]* ]] && (( now - mtime < MAT_STATUS_CACHE_TTL )); then
      cat "$MAT_STATUS_CACHE"
      return 0
    fi
  fi

  tmp="${MAT_STATUS_CACHE}.$$.$RANDOM"
  if command mat status --json > "$tmp" 2>/dev/null && mv "$tmp" "$MAT_STATUS_CACHE" 2>/dev/null; then
    cat "$MAT_STATUS_CACHE"
  else
    rm -f "$tmp"
  fi
}

mat_statusline() {
  mat_status_cached | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{try{const r=JSON.parse(s||"{}");const profiles=(r.activeProfiles||[]).map(p=>`${p.cliId}:${p.profileName}`).join(" ");const sessions=r.sessions||{};const warn=(sessions.orphan||sessions.unknown)?`⚠${sessions.orphan||0}/${sessions.unknown||0}`:"";const out=[profiles,warn].filter(Boolean).join(" ");if(out)process.stdout.write(out);}catch{}});' 2>/dev/null
}

zsh RPROMPT에서 사용:

source ~/.config/mat/statusline.zsh
setopt prompt_subst
RPROMPT='$(mat_statusline)'

tmux에서 사용:

set -g status-right '#(zsh -lc "source ~/.config/mat/statusline.zsh && mat_statusline")'

Starship에서 사용:

[custom.mat]
command = 'zsh -lc "source ~/.config/mat/statusline.zsh && mat_statusline"'
when = 'command -v zsh >/dev/null 2>&1 && command -v mat >/dev/null 2>&1 && command -v node >/dev/null 2>&1'
format = '[$output]($style) '
style = 'cyan'

기본 formatter는 codex:work gemini:personal ⚠1/0처럼 짧게 출력한다. 경고 숫자는 orphan/unknown session 수다. 다른 모양이 필요하면 Node formatter 부분만 바꾸면 된다.

지원 CLI (자격증명 디렉토리를 env로 재배치할 수 있는 것):

CLI env var
Codex CODEX_HOME
Qwen Code QWEN_HOME
Kimi KIMI_SHARE_DIR
Crush CRUSH_GLOBAL_CONFIG + CRUSH_GLOBAL_DATA
Gemini CLI GEMINI_CLI_HOME (.gemini envSubdir)
Claude Code (Linux 전용) CLAUDE_CONFIG_DIR
OpenCode (EXPERIMENTAL) XDG_DATA_HOME (opencode envSubdir; broad XDG side effect)

mat session start 미지원 (안전한 자격증명 재배치 env 없음; session start가 명시 에러): macOS claude(Keychain service name env override 불가), aider(provider env / CLI args / project-local config 등 세션 재배치 불가 채널; 대신 더 좁은 mat session run aider partial support 사용), goose(keychain/OS-keyring 자격증명은 env 디렉토리 리다이렉트 불가), grok(profile-swap-only ~/.grok/auth.json; config/env/project/MCP 우회는 별도 session-isolation 설계 필요), Google Antigravity / agy(native keyring + 안정적인 CLI 전용 credential redirect 부재; HOME redirect는 너무 광범위), 그리고 사용자 플러그인 CLI(빌트인 전용 신뢰경계).

종료 코드는 mat exec와 동형: 0 성공, 2 사용법 에러, 74 재캡처 실패, 128+N 자식 시그널 N (self-raise), 그 외 자식 종료 코드 전달.

한계 (사용 전 필독):

mat freshness — swap 전 안전성 점검

mat freshness [<cli>] [--profile <name>] [--json] [--check-only]

라이브 자격증명과 활성(또는 지정) 프로필 저장본을 비교해 swap 전에 drift를 보고한다. <cli>를 생략하면 현재 활성 프로필이 있는 모든 builtin/plugin CLI를 보고한다. CI chain(mat freshness && deploy.sh)으로 wrong-profile 복원으로 인한 OAuth refresh_token revoke 사고를 사전 차단할 수 있다.

# 긴 Claude 세션 시작 전 빠른 점검
mat freshness claude

# 특정 프로필 검사 (JSON 출력, CI 친화)
mat freshness codex --profile work --json

# statusline/dashboard 모드: 같은 보고서를 출력하되 unsafe 상태여도 실패하지 않음
mat freshness --check-only

각 source는 4-state로 분류한다 — fresh(byte 동일), rotated(토큰 회전), stale(identity 변경 — 다른 계정, swap 시 revoke 위험), inflight(multi-source CLI의 부분 갱신 race — 잠시 후 재시도). rotated는 adapter가 high/medium confidence로 identity 유지를 확인할 때만 swap 안전이다. fallback byte-diff 결과나 parser 실패는 "바이트가 달라졌다"까지만 말할 수 있으므로, low-confidence rotated는 exit-code 기준 unsafe로 취급되어 --check-only가 아니면 1을 반환한다.

--check-only는 read-only 모니터링 모드다. stale / low-confidence rotated / inflight 결과를 그대로 출력하지만 exit code는 0으로 유지해 프롬프트, statusline, dashboard가 경고를 표시하면서도 shell 흐름을 끊지 않게 한다. 사용 오류나 source 읽기 실패는 숨기지 않는다.

종료 코드:

코드 의미
0 모든 source가 fresh 또는 high-confidence rotated — swap 안전
1 하나 이상의 source가 stale, low-confidence rotated, inflightswap 전 조치 필요 (--check-only 제외)
2 사용 오류
74 내부 검사 실패 (source 읽기 에러 등)

mat doctor — read-only 안전 진단

mat doctor [--json]

알려진 모든 CLI에 대해 metadata-only 안전 진단을 실행한다. doctor는 활성 프로필 상태, 저장된 경우 sanitized capture-time identity metadata, 프로필 디렉토리 존재 여부, 비밀값을 읽지 않고 확인 가능한 라이브 source 존재 여부, session 지원 플래그, plugin 경고, provider API-key env var나 project-local config 파일 같은 high-confidence ambient override 채널을 보고한다.

mat doctor는 저장본/라이브 자격증명 내용을 비교하지 않고, identity backfill을 위해 profile credential file을 파싱하지 않으며, secret 값을 출력할 수 있는 OS keyring entry 조회도 하지 않는다. OAuth rotation까지 포함한 deep 비교가 필요하면 명시적으로 mat freshness <cli>를 사용한다.

# 사람이 읽는 보고서
mat doctor

# CI/statusline 친화 JSON 보고서
mat doctor --json

CLI별 분류 신뢰도는 README 상단 OAuth Rotation 안전성 매트릭스 참고.

mat support / mat explain — CLI 지원 경계 설명

mat support <cli> [--json]
mat explain <cli> [--json]

하나의 CLI에 대해 mat이 정확히 무엇을 지원하고 왜 그런지 보여준다. 보고서에는 profile swap, freshness/drift 점검, mat session start, mat session run, source type(라이브 경로나 자격증명 값 제외), capture-time profile identity signal의 static 지원 여부, ambient/project override 위험, 지원 판단의 마지막 upstream 확인 가정이 포함된다.

explainsupport의 alias다. agy처럼 의도적으로 blocked된 CLI도 profile-swap 대상이 아니더라도 설명 가능하다. 사용자 plugin CLI는 profile-swap only + fallback freshness로 표시되며, 신뢰된 session boundary는 없다고 보고한다.

mat support codex
mat support aider --json
mat explain agy

데이터 저장 위치

~/.multi-account-tool/
├── config.json                   # 활성 프로필 포인터 + 플래그
├── app.log                       # best-effort TUI 경고 / 진단 로그
├── audit.jsonl                   # best-effort redacted session lifecycle audit 로그
├── cli-defs/                     # 사용자 플러그인 (선택) — "새 CLI 추가하기" 참고
│   └── <id>.json
├── locks/
│   ├── <cli>.lock/               # CLI별 `mat exec` lock (stale 자동 회수)
│   └── recapture/
│       └── <cli>/<profile>.lock/ # `mat session` 프로필 재캡처 advisory lock
├── sessions/
│   └── <session-id>/             # 실행 중/orphan `mat session` 임시 디렉토리 + session.json
└── profiles/
    ├── claude/                   # credentials.json (macOS Keychain 백업, 평문 JSON)
    │   ├── personal/
    │   │   ├── credentials.json
    │   │   └── meta.json
    │   └── work/...
    ├── codex/                    # auth.json
    ├── gemini/                   # oauth_creds.json + google_accounts.json
    ├── aider/                    # aider.yml
    ├── kimi/                     # config.toml
    ├── qwen/                     # qwen-settings.json + qwen.env (prefix 적용된 saveAs)
    ├── crush/                    # crush-config.json + crush-data.json (config + data 레이어)
    ├── opencode/                 # auth.json (OS 공통 XDG 경로)
    ├── goose/                    # goose-keyring.json (macOS Keychain / Linux Secret Service) + goose-secrets.yaml + goose-config.yaml
    └── grok/                     # grok-auth.json (profile-swap-only ~/.grok/auth.json)

파일은 0600, 디렉토리는 0700 권한으로 생성된다.


보안

수용한 trade-off (의도된 한계)

기본 보호 장치

사용을 권하지 않는 환경


새 CLI 추가하기

두 가지 방법.

1. 사용자 플러그인 — 코드 변경 불필요 (개인 사용 권장)

~/.multi-account-tool/cli-defs/<id>.json 파일을 만든다. 임의 CLI 추가용 템플릿 예:

{
  "id": "my-cli",
  "name": "My CLI",
  "sources": [
    { "type": "file", "path": "~/.config/my-cli/credentials.json", "saveAs": "credentials.json" }
  ]
}

파일을 직접 쓰지 않고 starter JSON을 만들 수도 있다:

mkdir -p ~/.multi-account-tool/cli-defs
mat plugin scaffold my-cli > ~/.multi-account-tool/cli-defs/my-cli.json
mat plugin validate ~/.multi-account-tool/cli-defs/my-cli.json
mat plugin validate --json   # 설치된 ~/.multi-account-tool/cli-defs/*.json 전체 검증

mat plugin validate정적 JSON/schema/lint 검사다. credential 파일을 읽거나 Keychain/Secret Service secret 값을 조회하지 않고, upstream CLI가 의도한 credential source를 우선 사용할지 증명하지도 않는다. 통과했다는 뜻은 정적 검증 통과이지 security-certified라는 의미가 아니다. JSON report는 schemaVersion: 1이며 exit code는 error 없음 0, validation/read/parse error 1, 사용법 오류 2다. 너무 넓은 file path나 account 없는 generic keychain service 같은 위험하지만 호환되는 패턴은 warning으로 보고한다.

mat은 시작 시 해당 디렉토리의 모든 *.json을 로드한다. 잘못된 plugin은 경고 후 skip되며, mat 본체는 정상 동작한다. 빌트인 CLI(claude, codex, gemini, aider, kimi, qwen, crush, opencode, goose, grok) id와 충돌하면 plugin이 무시된다(보안).

필드 규칙:

Plugin은 profile-swap 정의 전용이다. session, sessionRun, env policy, ambient credential scrub 규칙, project override hard-stop을 정의할 수 없고, 사용자 plugin CLI는 신뢰된 mat session start/run 경계가 아니다.

2. 빌트인 추가 — mat repo PR 필요

src/core/cli-defs.ts에 항목 추가:

{
  id: 'foo',
  name: 'Foo CLI',
  sources: [
    { type: 'file', path: '~/.foo/credentials.json', saveAs: 'credentials.json' }
  ]
}

mat과 함께 배포되어야 할 커뮤니티 CLI용. PR 환영.


변경 이력

릴리스 이력과 주요 변경 사항은 CHANGELOG.md 참고 (Keep a Changelog 형식, Semantic Versioning).

로드맵

v0.4+ 계획은 ROADMAP.md 참고:


라이선스

MIT — LICENSE