왜 만들었나
- Claude Code, Codex, Gemini 같은 CLI를 여러 계정(개인 / 회사 / 팀)으로 쓸 때
- 컨텍스트를 바꿀 때마다
logout/login을 반복하기 지쳤을 때 - 작업을 시작하기 전에 현재 활성 계정을 명확히 확인하고 싶을 때
어떻게 동작하나
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/ 직하의 고정 파일 5개/제한 디렉터리 2개 |
프로필 swap 전용; v1.43 고정 경로만, discovery/session 격리 없음. 0.8.1 이전 프로필에는 provider artifact가 없음 — 각 프로필은 그 계정으로 로그인한 상태에서 재캡처 |
| 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/mediumconfidence로 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 session은 GEMINI_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 qwen은 QWEN_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로 확인한 ~/.config/goose/ 직하 파일 5개(gemini_oauth/tokens.json, chatgpt_codex/tokens.json, kimicode/token.json, xai_oauth/tokens.json, huggingface/oauth/tokens.json)와 제한된 디렉터리 2개(githubcopilot/, databricks/oauth/)만 swap한다. 0.8.1 이전 릴리스는 업스트림에 존재하지 않는 ~/.config/goose/providers/... 를 한 단계 더 깊이 보고 있었으므로 그 프로필에는 provider artifact가 하나도 없다. 각 Goose 프로필은 그 프로필의 계정으로 Goose에 로그인한 상태에서 재캡처하라 — provider 캐시는 항상 '지금 로그인한 계정' 것이다. carry-over 경고가 뜬 직후에는 재캡처하지 마라: 그 시점의 라이브 캐시는 여전히 직전 계정 것이므로, 캡처하면 방금 전환한 프로필에 다른 계정 토큰을 저장하게 된다. 프로필에 대응 값이 없는데 라이브에 provider 캐시가 있으면 조용히 건너뛰지 않고 "이전 계정 자격증명이 남아 있음"으로 보고한다. 이를 우회·재배치할 수 있는 ANTHROPIC_HOST, HF_TOKEN, GOOSE_PATH_ROOT, XDG_CONFIG_HOME은 ambient warning으로 보고하며, 재배치된 Goose config 디렉터리는 미지원이다. Goose는 이 캐시 디렉터리를 명시 mode 없이 만들기 때문에 group-writable umask면 0775가 되어 MAT의 private-parent 검사가 fail-closed로 막는다 — mat doctor로 어느 artifact인지 확인한 뒤 그 디렉터리에 chmod g-w,o-w를 적용하라. discovery/session/run은 열지 않고 schema는 redacted fixture 승인 전 opaque이며 Windows는 미지원이다. |
| Grok Build | ✅ | ✅ | ⚠️ 미검증 | 현재 지원은 기본 signed-in browser/OIDC 토큰 파일 ~/.grok/auth.json만 grok-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/검토하라.
전환 흐름 (데이터 손실 없음)
- swap 전 freshness 점검 — 라이브 자격증명이 활성 프로필 저장본과 drift(OAuth refresh 토큰 회전 등)된 상태면, 아래 1~3 단계 전에 재캡처 / 폐기 / 취소 dialog가 먼저 표시된다. CLI별 분류 신뢰도는 위의 "OAuth Rotation 안전성 매트릭스" 참고.
- 현재 라이브 자격증명을 현재 활성 프로필에 자동 스냅샷
- 선택한 프로필의 저장된 자격증명을 라이브 위치로 원자적으로 복원
- 활성 프로필 포인터 업데이트
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 프로필로 가져올지 묻는다. 한 번 답하면 다음 실행부터 자동으로 다시 묻지 않는다 (수동 캡처는 언제나 가능).
새 계정 추가하기
mat→ CLI 선택 →a(새 프로필) → 이름 입력 (예:work)- 새 프로필에서
Enter를 눌러 활성화한다. 라이브 자격증명이 활성 프로필의 저장본과 drift(OAuth refresh 토큰 회전 등)된 상태라면 swap 직전에 재캡처 / 폐기 / 취소 dialog가 표시된다 — 위의 전환 흐름 + OAuth Rotation 안전성 매트릭스 참고. - 별도 터미널에서 해당 CLI의 로그인 명령 실행 (
claude,codex,gemini등). 라이브 자격증명이 새 계정 것으로 덮어쓰인다. mat으로 돌아와 같은 프로필에서c(캡처)를 누른다. 새 라이브 자격증명이 프로필에 저장된다.- 이후로는
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
동작:
<cli>에 이미 활성 프로필이 설정되어 있어야 한다(먼저 TUI로 라이브 자격증명 캡처).- CLI별 lockfile(
~/.multi-account-tool/locks/<cli>.lock)로 동일 CLI의mat exec가 동시에 race하지 않도록 직렬화한다. 비정상 종료로 남은 stale lock은 자동 복구한다. - 자식에
SIGINT/SIGTERM/SIGHUP을 전달하고, 자식의 종료 코드/시그널을 그대로 반영한다. - 종료 시 라이브 자격증명을
<profile>으로 재캡처한 뒤 원래 활성 프로필로 복원한다(<cmd>가 OAuth refresh rotation 등으로 토큰을 갱신했을 가능성 보존). 재캡처는 기본 10s timeout(MAT_EXEC_RECAPTURE_TIMEOUT_MSenv override)으로 keychain prompt hang을 제한한다. - 복원은
finally블록에서 일어나 정상 종료, 에러, trap 가능 시그널 모두에서 실행된다. 단mat자체가SIGKILL(또는SIGSEGV/SIGBUS등 trap 불가 시그널)을 받으면 복원은 일어나지 않는다 — 다음mat실행 시 stale lock을 자동 회수하고, 사용자에게 "라이브 자격증명이 이전 활성 프로필이 아닌<profile>의 것일 수 있음"을 stderr로 안내한다(정책 B: warn + drop).
이는 시간 격리이지 세션 격리가 아니다. 자식이 실행되는 동안 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 / 75는 mat 자체의 에러 모델로 예약되어 있다(spawn 전 검증 / lock 경합 / spawn 후 복원 실패). 그 외의 128 미만 non-zero 코드는 모두 자식의 종료 코드를 투명하게 전달한다. 74가 mat의 복원 실패인지 자식의 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.toml과 skills/가 격리된 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), 그 외 자식 종료 코드 전달.
한계 (사용 전 필독):
- 자격증명은 격리, 복사된 config는 세션 로컬.
mat은 자격증명을 세션에 복사하고 종료 시 자격증명만 재캡처한다. 비밀값이 아닌 read-mostly data는 좁은 allow-list로 복사될 수 있지만, 이 복사본도 격리된 스냅샷이며 write-back 되지 않는다. 현재 Codex는config.toml과skills/스냅샷을 세션에 받아 skill을 사용할 수 있고, 세션 안에서 바꾼 config나 skill은 종료 시 버려진다. symlink, hardlink, 특수파일, 너무 깊거나 큰 tree, 항목 수가 너무 많은 tree는 shell spawn 전에 실패한다. 그 외 history·cache·session·대부분 config는 세션 안에서 만들어지고 종료 시 폐기된다. 실제 config/history 보존이 중요한 장기 단일 계정 작업은mat exec, 동시 다계정은mat session을 권장. - 세션 자식은 ambient credential env를 scrub한다. profile copy-isolation이 부모 shell의 env로 우회되지 않도록
mat session start와mat session run은 고신뢰 provider/API endpoint env 및 AWS/GCP credential-chain env를 상속하지 않는다. 가능한 경우 AWS/GCP shared-credential fallback은 무해한 값으로 hardening한다.GITHUB_TOKEN같은 broad non-provider token은 scrub하지 않는다. 의도적으로 provider env가 필요하다면mat session start의 경우 세션이 열린 뒤 interactive 세션 안에서 직접 export 하라. command-scopedmat session run에는 시작 후 export할 shell이 없으므로 필요한 env는 호출되는 CLI가 지원하는 컨텍스트에서 설정되어야 하며, Aider/OpenCode safer-run은 알려진 provider env/config 우회를 전달하지 않고 hard-stop할 수 있다. - Aider
session run은 partial support;session start aider는 계속 미지원.mat은 프로필의aider.yml을 session command directory에 materialize한 뒤 builtinaiderexecutable만 실행하면서--config <session>/command/aider.yml와 빈--env-file <session>/command/.env를 강제한다. child의 AWS/Google ambient credential-chain fallback도 scrub한다(AWS_SHARED_CREDENTIALS_FILE=/dev/null,AWS_CONFIG_FILE=/dev/null,AWS_EC2_METADATA_DISABLED=true,GOOGLE_APPLICATION_CREDENTIALS=/dev/null등). 사용자--config/-c,--env/--env-file,--api-key, provider key/endpoint 인자(--openai-api-key,--anthropic-api-key,--openai-api-base,--openai-base-url등),--set-env, model sidecar 인자(--model-settings-file,--model-metadata-file), ambientAIDER_*env,OPENAI_API_KEY,ANTHROPIC_API_KEY,GEMINI_API_KEY,OPENROUTER_API_KEY,DEEPSEEK_API_KEY,OPENAI_API_BASE같은 provider credential/endpoint env,AWS_ACCESS_KEY_ID,AWS_PROFILE,GOOGLE_APPLICATION_CREDENTIALS,VERTEXAI_PROJECT등 AWS/Google/Vertex credential-chain env, generic*_API_KEY/*_TOKEN/*_SECRET_KEY, credential-bearing home/project.env후보, non-empty/symlinked~/.aider/oauth-keys.env, non-empty/symlinked Aider model sidecar(.aider.model.settings.yml,.aider.model.metadata.json), profile 내부 model-sidecar pointer, profileset-env, host AWS/Google credential chain에 의존하는 Bedrock/Vertex model selector/listing 요청 또는 alias는 hard-stop한다. 이는 command-scoped credential boundary이며 Aider home/config 전체 격리 claim이 아니다. - Qwen은 profile-swap + advisory session-start 지원이며
session run qwen은 비활성화되어 있다.mat session start qwen은QWEN_HOME을 격리된 profile copy로 재지정하지만, Qwen의 shell environment, project/ancestor dotenv 탐색, legacy/home configuration, settings 기반 auth routing, interactive working-directory 변경을 가둔다는 주장은 하지 않는다. Ambient detection은 app 기반 switch,mat exec,mat doctor에서만 실행되고session start에서는 실행되지 않는다. 비대화형 profile 선택에는 제어된 profile swap(mat exec qwen <profile> -- qwen ...)을 사용하되 command-scoped credential boundary로 간주하지 않는다. 고정한 Qwen v0.19.10 all-auth-source 계약을 fail-closed parse/recheck할 수 있을 때까지mat session run qwen은 계속 제공하지 않는다. - OpenCode
session start는 계속 EXPERIMENTAL; 가능하면session run권장. OpenCode는 upstream에OPENCODE_DATA_DIR가 없어XDG_DATA_HOME을 사용한다.session start에서는 이 env가 subshell 전체에 적용되므로 다른 XDG 도구(예: Crush)가 세션 안에서 data/credential을 쓰면 ephemeral 세션 디렉토리에 기록됐다가 종료 시 사라질 수 있다.mat session run opencode ...는 더 좁게 builtinopencodeexecutable만 실행하고, child의 AWS/Google ambient credential fallback을 scrub하고 OpenCode.claudeprompt/skills loading을 끄며, 알려진 local config/env/plugin/tool/MCP/command/argv 우회가 있으면 hard-stop한다(attach,pr,--dangerously-skip-permissions,--share,--command,--file/-f,--dir/--cwd/path-like directory/symlink 같은 cwd/project-directory arg,OPENCODE_AUTH_CONTENT,OPENCODE_CONFIG*,OPENCODE_DB,OPENCODE_MODELS_*,OPENCODE_TEST_HOME,OPENCODE_TEST_MANAGED_CONFIG_DIR,OPENCODE_TUI_CONFIG,OPENCODE_PERMISSION,*_API_KEY/AWS key/GOOGLE_APPLICATION_CREDENTIALS/SNOWFLAKE_CORTEX_PAT같은 provider credential env 또는 project.envassignment,OPENCODE_DISABLE_CLAUDE_CODE*로 차단되는.claudeprompt/skills fallback,{file:...}substitution/apiKey/credential header·option/provider env reference/provider endpoint override/providernpm/AWS profile/instructions/skills/reference(s)/share/deprecatedmode/agentprompt·permission·tools/plugin/mcp/shell/formatter·lspcommand 설정을 포함한 legacyconfig,config.json/opencode.json{,c}/tui.json{,c}global/managed/project/home.opencodeconfig, local plugin/tool/command/mode/agent/skill directory, OpenCodepackage.jsonmanifest, symlink/unreadable config 후보, macOS managed preference 후보). - **
SIGKILL**은 세션 디렉토리를 orphan으로 남긴다(trap 불가,mat exec와 동일). 다음mat session호출이 회수한다(소유 프로세스 사망 — 프로세스 시작서명으로 PID 재사용까지 식별 — 그리고 세션 시작시각·디렉토리 mtime 둘 다 1h 초과 시). - 부모 신뢰 전제: 격리는
~/.multi-account-tool과 그 부모가 신뢰됨을 전제한다.~/.multi-account-tool자체가 symlink로 바꿔치기된 경우는 범위 밖이다. - 같은 프로필 동시 2세션: 재캡처는 best-effort 프로필 단위 advisory lock(
locks/recapture/<cli>/<profile>.lock)으로 backup → stage → commit 구간을 가능한 한 직렬화한다. lock 획득 실패/timeout 시에는 세션 종료를 막지 않고 lock-free 2-phase commit으로 degrade한다. 이 경우 단일 자격증명 CLI는 last-writer-wins, 멀티 자격증명 CLI(Qwen/Crush)는 같은 계정의 서로 다른 세션 파일이 일시적으로 섞일 수 있다. wrong-account credential을 쓰지는 않으며 다음 사용 시 자가 치유된다. 결정적 재캡처가 필요하면 터미널마다 다른 프로필 사용을 권장. - **
mat session stop**은 소유 프로세스의 신원(PID + 시작서명)을 확인할 수 있을 때만SIGTERM을 보낸다. 확인 불가 시(드묾 — 예:ps미사용 가능) PID를 재사용한 무관 프로세스를 죽일 위험을 피해 세션을 건드리지 않고 재시도를 안내한다.
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, inflight — swap 전 조치 필요 (--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 확인 가정이 포함된다.
explain은 support의 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 (의도된 한계)
Keychain ACL 호환 모드 — macOS built-in Keychain source는 Claude Code/Goose 같은 upstream CLI가 swap된 항목을 계속 읽을 수 있도록 기본적으로
security add-generic-password -A를 유지한다. 이 경우 같은 UID의 프로세스(악의적npm postinstall등)도 항목을 조용히 읽을 수 있다. 해당 실행에서 no--A쓰기를 강제하려면MAT_KEYCHAIN_RESTRICT_ACL=1을 설정한다. 사용자/plugin Keychain source가 legacy broad ACL을 필요로 하면MAT_KEYCHAIN_ALLOW_ANY_APP=1로 명시 허용할 수 있다. 더 좁은-T <path>화이트리스트 모드는 향후 hardening 대상이다.OAuth 토큰 평문 백업 —
~/.multi-account-tool/profiles/아래 OAuth 토큰이 평문 JSON으로 저장된다. 파일0600, 디렉토리0700권한이지만 디스크 백업/스냅샷에는 포함될 수 있다. Time Machine / iCloud / 클라우드 동기화 폴더에서 제외하길 권장:xattr -w com.apple.metadata:com_apple_backup_excludeItem true ~/.multi-account-tool명령행 인자 노출 —
security add-generic-password -w <value>가 평문 토큰을argv로 받는다(securityCLI 자체 한계).ps -ef/ BSM audit / EDR 로그에 일시적으로 노출된다. audit / EDR가 활성화된 기업 환경에서는 사용을 권하지 않는다.
기본 보호 장치
- 모든 외부 명령은
spawn(argv)만 사용 — 셸 미경유, injection 차단 security는 절대경로/usr/bin/security만 호출(PATH shim 공격 방지)- 모든 파일 쓰기는 단일 atomic 헬퍼 (
.tmp → rename,O_EXCL + O_NOFOLLOW,0600) - config 변경은
mutateConfig헬퍼로 직렬화 (in-process race 차단) - 프로필 이름:
[a-zA-Z0-9가-힣_.-]{1,40}+ NFC 정규화 +./..///\/ NUL 명시 차단 - Keychain swap: 백업 → 정확 acct 매칭 delete → add. add 실패 시 자동 롤백, 롤백도 실패하면 에러 메시지에 함께 노출.
- multi-source CLI 복원은 부분 실패에 안전 (한 source 실패 시 이미 복원된 source를 라이브 백업으로 되돌림)
- 에러 메시지의 JWT 및 50자+ base64-like 시퀀스는 redact 처리하고, session allow-list 경로는 stderr 노출 전 terminal control char를 sanitize
- 자격증명 소유권을 경로 표기가 아니라 파일시스템 identity 로 판정한다. plugin 이 별칭(디렉토리 symlink, 파일 symlink, hardlink)을 선언해서 builtin CLI 의 라이브 자격증명에 도달할 수 없다. 경로는 해석 후 비교하고, hardlink 는 별도 dev/ino 축으로 잡으며, 해석에 실패한 경로는 통과가 아니라 거부다 — 의도된 예외가 하나 있다: goose 예약구역 루트 자체를 해석할 수 없으면 구역 판정은 어휘 결과로 되돌아간다. 그 자리에서 거부했더니 예약구역과 무관한 파일까지 막혔기 때문이다. 판정은 plugin 로드 시점과 매 read/write/exists/remove 시점에 각각 이뤄지므로 로드 이후 별칭이 된 source 는 다음 접근에서 거부된다. 다만 경합이 없다는 보장은 아니다 — goose 가 아닌 파일 source 는 그 판정과 실제 open 사이에 경로를 고정하지 않으므로, 그 창 안에서 조상이 교체되면 탐지하지 못한다. goose provider 캐시는 추가로 부모 dev/ino 를 pinning 하고
O_NOFOLLOW로 연다. - 의존성:
npm auditclean
stow/chezmoi 로 ~/.config 를 관리한다면 반대급부가 있다: Goose provider 캐시가 이제 해석된 실물 위치에 쓰이며, 그 위치가 dotfiles 저장소 안일 수 있다. 해당 경로가 .gitignore 에 있는지 확인하라.
사용을 권하지 않는 환경
- 공용 / 공유 워크스테이션
- 다중 사용자 호스트
- 관리형 / audit·EDR 활성 기업 기기
- 클라우드 동기화 폴더 안의 home 디렉토리
새 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이 무시된다(보안).
필드 규칙:
id: 영문 시작 + 영숫자/_/-, 1~32자(빌트인 id와 중복 불가).name: 비어있지 않은 임의 문자열 (표시용).sources[].type:'file','keychain'(macOS Keychain),'os-keyring'(Linux Secret Service),'env-secret'(metadata-only; product runtime blocked),'win-credential'(Windows Credential Manager generic credential primitive).sources[].saveAs: ASCII 파일명, 1~64자 ([a-zA-Z0-9._-]).sources[].path(file): 비어있지 않은 문자열(~/자동 확장).sources[].service(keychain/os-keyring): 비어있지 않은 credential service 이름.sources[].account(keychain/os-keyring, 선택): service+account 항목 하나로mat의 조작 scope를 제한. generic / multi-account service에 필수(예: Goose의goose/secrets, 동일 service의 여러 entry를 가진 CLI) — 미지정 시mat이 잘못된 account를 잡을 위험이 있다. 검증: 비어있지 않은 문자열, 제어문자 차단. 단일-account service는 생략 가능(기존 동작 유지).sources[].backend(os-keyring, 선택):'auto'또는'secret-service'.sources[]의type: 'win-credential': 정확한 key만 허용 —type,targetName,credentialType: 'generic', 필수account, 필수persist('session' | 'local-machine' | 'enterprise'),saveAs. Windows lookup identity는targetName + credentialType이며,account는 lookup selector가 아니라 필수 UserName metadata guard다. 비-Windows host에서는 detector/freshness/doctor/support가 이 source를 일반 missing이 아닌 unsupported/blocked로 보고한다. 이는 package-level Windows support, built-in Windows CLI support, Copilot support, 또는 신뢰된mat session경계를 의미하지 않는다.
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 참고:
커뮤니티 CLI 정의를 위한 플러그인 메커니즘✅ (v0.3)Aider 빌트인 지원✅ (v0.3) +Kimi / Qwen / Crush / OpenCode✅ (v0.3.x)세션별 자격증명 격리✅ (v0.4.x —mat session start/list/stop: env 주입 + copy-isolate, 동시 다계정;@ictechgy/ltermv1.0.25+에서ltermprofile shim 통합 완료)✅ — command-scoped safer-run 기반.mat session run <cli> <profile> -- [cli-args...]frameworkOpenCode hard-stop probe✅;Aider forced config/env-file partial-run✅.Antigravity auth-store research artifact✅, 하지만 제품 지원은 upstream이 안정 auth-store, redirect, recapture 계약을 문서화할 때까지 blocked로 유지한다. Antigravity research note와 session-run R&D note 참고.- 빌트인 CLI 추가 확장 —
Goose✅ (v0.4.0 account-scoped Keychain; Linux Secret Service는os-keyringsource type으로 추가됨). Copilot / Amp는 보류 — Copilot/Amp research note 참고. Copilot은 명시적 account binding, application-state swap, ambient token fallback 정책, Windows Credential Manager 지원이 필요하다. Amp는 일반 file/keychain profile swap이 아니라 env-secret / command-scoped 실행 설계가 필요하다. Cursor Agent는 plugin 권장(keychain service name 공식 미공개). - Goose Linux: Linux에서
mat은 Goose의 기본secret-service백엔드(libsecret, GNOME Keyring/KWallet)를os-keyringsource(secret-toolCLI,goose/secrets)로 swap하고,~/.config/goose/*.yaml및 위에서 설명한 고정 v1.43 provider-cache 파일/디렉터리도 함께 swap한다. keyring 설정별 동작:- 기본(keyring): os-keyring source가 포함되며
secret-tool(libsecret-tools) + keyring daemon이 필요하다. 미설치이거나 daemon이 down/접근거부면 명시 에러 — yaml로 조용히 fallback 하지 않는다. Goose는 keyring에 libsecret 라이브러리(secret-toolCLI와 별도 패키지)로 접근하므로, CLI 부재가 keyring 미사용을 증명하지 못한다. 활성 keyring 사용자에게secrets.yaml을 조용히 swap하면 wrong-account가 된다. (도구 부재가 아니라) keyring 항목 자체의 부재는 정상 "not found"로 yaml로 넘어간다. - file backend: Goose가 keyring을 쓰지 않도록 의도적으로 설정한 경우
GOOSE_DISABLE_KEYRING=1을 사용한다.mat은 이 env가 존재하면(값 무관 —0/false/빈 문자열 포함) file backend로 본다 — Goose 자신의env::var(...).is_ok()판정과 동일. 그러면 keyring source(Linux=os-keyring, macOS=Keychain)를 생략하지만secrets.yaml,config.yaml, 고정 provider-cache source는 계속 swap한다.config.yaml만의keyring: false설정은 자동 감지하지 않으니 env도 함께 지정하라.
- 기본(keyring): os-keyring source가 포함되며
✅ (lterm claude --profile <name>같은 shim wrapperictechgy/light_terminalPR #144에서 구현)
라이선스
MIT — LICENSE