Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5677af9
fix(runtime): clear inherited web disables for opt-out
Bkankim Jun 28, 2026
2e119d8
feat(wallet): make credential mutations atomic
Bkankim Jun 28, 2026
6628c88
feat(accounts): add portable wallet lifecycle commands
Bkankim Jun 28, 2026
ea2fa79
feat(diagnostics): add offline wallet health checks
Bkankim Jun 29, 2026
425545c
fix(install): preserve portable wallet state
Bkankim Jun 29, 2026
761ddf0
docs(wallet): define the portable account wallet
Bkankim Jun 29, 2026
ac79d4c
ci(shell): verify macOS and Linux wallets
Bkankim Jun 29, 2026
9b53260
fix(ci): support runner shell toolchains
Bkankim Jun 29, 2026
468a1a3
fix(ci): prefer GNU stat before BSD fallback
Bkankim Jul 1, 2026
b949778
fix(runtime): initialize bare-launch label under nounset
Bkankim Jul 1, 2026
6e34895
fix(security): protect wallet credentials and lock ownership
Bkankim Jul 2, 2026
fe847af
fix(runtime): handle strict shells and relative state paths
Bkankim Jul 2, 2026
f2321a4
fix(install): secure launcher and shell onboarding
Bkankim Jul 2, 2026
8811ebf
ci(shell): run failure-path suites
Bkankim Jul 2, 2026
ad494b9
fix(ci): annotate intentional shadow function
Bkankim Jul 2, 2026
986538b
fix(runtime): close final shell and diagnostic gaps
Bkankim Jul 3, 2026
ae637b3
fix(install): reject ambiguous piped source paths
Bkankim Jul 4, 2026
1d0a4b5
docs(wallet): document dead-owner lock recovery
Bkankim Jul 4, 2026
20a12fe
fix(wallet): close lock and active-state races
Bkankim Jul 4, 2026
22074a1
fix(ci): annotate active lock signal handler
Bkankim Jul 4, 2026
cfa29db
fix(security): harden wallet inputs and metadata
Bkankim Jul 5, 2026
a22fd26
fix(security): isolate sticky rotation refresh
Bkankim Jul 6, 2026
3adf049
fix(ci): support hosted ShellCheck diagnostics
Bkankim Jul 6, 2026
3e7ca7f
fix(ci): exercise Perl fallback on Linux
Bkankim Jul 6, 2026
3343a2b
feat: cct refresh — 디스크 활성 라벨을 현재 셸에 재적용
Bkankim Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Shell tests

"on":
push:
branches:
- main
pull_request:

permissions:
contents: read

concurrency:
group: shell-tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os:
- macos-latest
- ubuntu-latest
defaults:
run:
shell: bash

steps:
- name: Check out repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
persist-credentials: false

- name: Install missing shell tools
run: |
if ! command -v shellcheck >/dev/null 2>&1; then
case "$(uname -s)" in
Darwin)
brew install shellcheck
;;
Linux)
sudo apt-get update
sudo apt-get install --yes shellcheck
;;
esac
fi

if ! command -v zsh >/dev/null 2>&1; then
case "$(uname -s)" in
Darwin)
brew install zsh
;;
Linux)
sudo apt-get update
sudo apt-get install --yes zsh
;;
esac
fi

- name: Run static checks
run: |
shellcheck cct.sh install.sh tests/cct_test.sh
bash -n cct.sh install.sh tests/cct_test.sh
zsh -n cct.sh
git diff --check

- name: Run full suite with a clean disable environment
run: |
env \
-u CLAUDE_CODE_DISABLE_ADVISOR_TOOL \
-u CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC \
-u CLAUDE_CODE_DISABLE_BACKGROUND_PLUGIN_REFRESH \
bash tests/cct_test.sh all

- name: Run failure-path suites with a clean disable environment
run: |
set -euo pipefail
unset CLAUDE_CODE_DISABLE_ADVISOR_TOOL
unset CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
unset CLAUDE_CODE_DISABLE_BACKGROUND_PLUGIN_REFRESH

CCT_TEST_CASE=install-failures bash tests/cct_test.sh install
CCT_TEST_CASE=live-lock bash tests/cct_test.sh wallet
CCT_TEST_CASE=lifecycle-signals bash tests/cct_test.sh accounts
CCT_TEST_CASE=lifecycle-refusals bash tests/cct_test.sh accounts
CCT_TEST_CASE=diagnostic-failures bash tests/cct_test.sh diagnostics

- name: Run full suite with inherited disable variables
env:
CLAUDE_CODE_DISABLE_ADVISOR_TOOL: "1"
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
CLAUDE_CODE_DISABLE_BACKGROUND_PLUGIN_REFRESH: "1"
run: bash tests/cct_test.sh all
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# 비밀/로컬 — 절대 커밋 금지
tokens.env
.claude/tokens.env
tokens.env.bak
.claude/tokens.env.bak
tokens.env.tmp.*
.claude/tokens.env.tmp.*
tokens.env.lock/
.claude/tokens.env.lock/
*.env
.credentials.json
*.key
Expand Down
69 changes: 67 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,68 @@
# Changelog

## Unreleased — security & correctness remediation
## Unreleased — portable Claude account wallet

### Product contract

cct is now documented as a **Portable Claude Account Wallet / 휴대용 Claude 계정
지갑**. Authenticate each Claude account once with `claude setup-token`, keep the
long-lived tokens in a local wallet outside Git, and explicitly switch with
`cct <label>` on macOS, Linux, or WSL2. It is not a proxy, orchestrator,
automatic router, or load balancer.

Setup-token lifetime and provider policy may change; this release does not promise
a fixed lifetime or permanent access. Expired, revoked, or exposed credentials must
be reissued for that account and replaced with `cct add <label>`.

| Command | Contract |
|---|---|
| `cct [claude args...]` | Launch the sticky active label, or the default label |
| `cct <label> [claude args...]` | Explicitly select an account and forward Claude arguments |
| `cct run <label> [claude args...]` | Explicitly launch a label even when it is a reserved command |
| `cct ls` / `cct list` | List account labels without token values |
| `cct add <label>` | Register or replace a setup-token through hidden input |
| `cct rm <label> [--force]` | Confirm and remove an account |
| `cct rename <old> <new>` | Rename an account without changing token bytes |
| `cct status` | Show local wallet and Claude metadata without network access |
| `cct doctor` | Report deterministic local `PASS/WARN/FAIL` health checks |
| `cct check [label]` | Validate token(s) through a real Claude call |
| `cct fp [label]` / `cct who [label]` | Compare account fingerprints through a real call |
| `cct active` | Show the sticky active label |
| `cct refresh` | Re-apply the on-disk active label to the current shell environment |
| `cct off` | Clear sticky state and current-shell cct auth variables |
| `cct help` | Show the built-in command contract |

General command errors use `1` for runtime/state failure and `2` for usage or label
errors. `check` uses `0` for valid, `1` for invalid/unavailable, and `2` for a
missing token. `doctor` uses `0` when there is no FAIL, `1` for health failures,
and `2` for invocation misuse.

### Wallet safety

- All wallet mutations use a mode-`600` same-directory temporary file and atomic
replace, with a mode-`600` rolling backup at `tokens.env.bak`.
- `tokens.env.lock/` serializes changes. Every live owner PID remains busy
regardless of the recorded epoch's age. A later mutation reclaims only valid
owner metadata whose PID is dead, after rechecking that the owner still
matches. The epoch is diagnostic data, not a timeout; diagnostics report lock
state without recovering or modifying it.
- `rm` and `rename` update wallet and active state as a recoverable transaction.
An active-state failure restores the verified wallet backup.
- `status` and `doctor` are offline and redact credentials.
- Reinstall preserves the wallet, rolling backup, active state, and in-progress
lock/temp files. Ignore coverage includes all of those credential-bearing paths.
- The plaintext wallet and backup remain password-sensitive. Use mode `600`,
full-disk encryption, encrypted transfer such as a password manager, and never
plaintext cloud sync or Git. On WSL2, keep them out of `/mnt/c`.

### Account lifecycle

- Added `cct run <label> [claude args...]`, including a compatibility escape for
legacy accounts whose labels collide with commands.
- Added confirmed/forced `cct rm` and atomic `cct rename`.
- Added offline `cct status` and deterministic `cct doctor`.

### Security & correctness remediation

Verified-bug remediation across `cct.sh` and `install.sh`.

Expand All @@ -24,7 +86,8 @@ Verified-bug remediation across `cct.sh` and `install.sh`.
back to `CCT_DEFAULT_LABEL`). Set `CCT_STICKY=0` for the old per-process inline behavior.
- **Strict label rules** — labels must match `[a-z0-9_][a-z0-9_]*`. Dashes, uppercase
letters, spaces, `@`, and non-ASCII labels are rejected. Labels that collide with a
subcommand (`help ls list add check fp who`) are rejected (`use` is still allowed).
subcommand (`help ls list add run rm rename status doctor check fp who off active`)
are rejected (`use` is still allowed).
`cct`, `cct check`, and `cct fp` now apply the same validation, so invalid labels cannot
alias an existing normalized token key.

Expand Down Expand Up @@ -70,6 +133,8 @@ a clean `[a-z0-9_][a-z0-9_]*` label.
fallback label for a bare `cct`; `CCT_STICKY=0` disables the sticky active profile;
`CCT_ACTIVE_FILE` overrides the active-profile state path.
- **`cct active` / `cct off`** — show or clear the sticky active profile.
- **`cct refresh`** — re-apply the on-disk sticky active label to the current shell,
so an already-open terminal follows a switch (or `cct off`) made in another terminal.
- **Claude Code 2.1.185+ token-mode guard** — labeled `cct <label>` launches now
suppress Advisor/background plugin refresh/nonessential web calls by default because
`claude setup-token` long-lived OAuth tokens are inference-only in current Claude Code.
Expand Down
Loading
Loading