Multi-account wrappers and helpers for Codex and Claude Code:
codex-as NAME [codex arguments...]andclaude-as NAME [claude arguments...]run the native CLI as a named account, with private stored credentials and the shared everyday configuration and history;- three Claude Code helpers: a status line, terminal notifications and a Git hook guard.
It installs on top of an existing codex or claude and never installs,
updates or replaces either. Install it as a managed install that follows the
newest patch release of a minor line and updates itself, link a checkout of
your own with install.sh, or link or copy its files.
- Linux or macOS. On native Windows only the Node helpers are tested, and
install.ps1installs only them; it needs PowerShell 7 and Git. - Python 3.11 or newer, standard library only (run as
python3 -I -B). - Bash for
codex-as(3.2 is enough), POSIXshforclaude-asandinstall.sh, and ordinary POSIX utilities. The managed install also needs Git andtar. - Codex CLI 0.161.0 or newer for
codex-as, with no upper limit. Claude Code forclaude-as; its token capture is audited for 2.1.296, and other versions fall back to an explicit manual paste. - Node.js 22 or newer for the helpers; Git is optional for the status line.
- Optional: bash-completion or zsh for account-name completion.
A managed install follows one channel, a MAJOR.MINOR line such as 0.6:
it installs the highest vMAJOR.MINOR.PATCH release of it and later moves
to newer patches only. Install, update and remove it with:
curl -fsSL https://raw.githubusercontent.com/FridrichMethod/coding-agent-kit/main/install.sh | sh -s -- --channel 0.6
"${XDG_DATA_HOME:-$HOME/.local/share}/coding-agent-kit/current/install.sh" --update
"${XDG_DATA_HOME:-$HOME/.local/share}/coding-agent-kit/current/install.sh" --uninstall # --purge: also the managed rootOn native Windows, with PowerShell 7 and Git, install.ps1 does the same for
the three helpers, the only part that runs there:
$f = Join-Path ([IO.Path]::GetTempPath()) "coding-agent-kit-install-$PID.ps1"
Invoke-WebRequest -UseBasicParsing https://raw.githubusercontent.com/FridrichMethod/coding-agent-kit/main/install.ps1 -OutFile $f
pwsh -NoProfile -ExecutionPolicy Bypass -File $f -Channel 0.6; Remove-Item $f
pwsh -NoProfile -ExecutionPolicy Bypass -File "$env:LOCALAPPDATA\coding-agent-kit\install.ps1" -Update
pwsh -NoProfile -ExecutionPolicy Bypass -File "$env:LOCALAPPDATA\coding-agent-kit\install.ps1" -Uninstall # -Purge: also the managed rootThese commands, the paths below and the exit codes are the stable interface
for scripts and other repositories that install the kit; they change only
in a minor release. The same bootstrap command installs, re-installs and,
with another --channel, switches the channel.
install.sh runs as a managed install when no checkout is next to it (piped
to sh), with --managed, --channel, --update or --purge, and always
in a release of its managed root:
install.sh --managed [--channel MAJOR.MINOR] [--update] [--prefix DIR]
install.sh --managed --reinstall [--channel MAJOR.MINOR] [--prefix DIR]
install.sh --managed --uninstall [--purge] [--prefix DIR]
The managed root is ${XDG_DATA_HOME:-~/.local/share}/coding-agent-kit/:
| Path | Content |
|---|---|
repo.git |
a bare clone of the repository |
releases/vX.Y.Z/ |
exported release trees: the current one and the one before it |
current |
a link to releases/vX.Y.Z, replaced by a rename |
channel, prefix |
the remembered channel and --prefix |
.lock |
the lock file |
and the install links these paths to current/, with DIR as for a
checkout below:
| Link | Target |
|---|---|
DIR/bin/codex-as, DIR/bin/claude-as |
current/bin/ |
${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-~/.local/share}/bash-completion}/completions/{codex-as,claude-as} |
current/completions/bash/ |
DIR/share/zsh/site-functions/{_codex-as,_claude-as} |
current/completions/zsh/ |
~/.claude/coding-agent-kit/{statusline,notify,check-git-hooks}.cjs |
current/helpers/ |
--channel MAJOR.MINORselects the channel; the managed root remembers it. Without it, a first install takes the channel of the newest release. A different--channelswitches to that channel's newest release, also to an older line.0.6is the first channel with managed installs.--updateneeds an existing managed install. It lists the repository's tags (git ls-remote, which writes nothing) and moves only to a newer patch of the remembered channel, or of the one--channelnames. With nothing newer it writes nothing and prints one line,coding-agent-kit: vX.Y.Z is already current (channel X.Y).It never leaves the channel. A run without--updatedoes the same, installs when nothing is installed yet and puts back a missing link.- A run exports the release it chose into
releases/and hands over to that release's owninstall.sh --activate. It checks every destination, renamescurrentover to itself, keeps the release before it, removes older ones and places the links that are not yet right; a switch that changes no link prints onlycoding-agent-kit: switched from vX.Y.Z to vX.Y.W (channel X.Y).Once a release is installed, every bootstrap or checkoutinstall.shhands its run tocurrent/install.sh, so only released code changes an install. --reinstallrepairs an install whose release is broken, which every other run would be handed to: the bootstrap (or anyinstall.sh) runs on its own, exports the newest patch of the channel again from a fresh clone, replaces the old clone and tree only once that worked, and hands that release's owninstall.sh --activatethe switch:curl -fsSL https://raw.githubusercontent.com/FridrichMethod/coding-agent-kit/main/install.sh | sh -s -- --reinstall.- A launcher resolves its links to its physical release directory, so a switch leaves a running session on the release it started with, which stays until the next switch.
- A destination is the managed install's own only when it is a symlink whose
text starts with the managed root's physical path and has no
.or..components, with the same refusals as a checkout install below. Links of a checkout install, Stow's links and every other path are refused before anything changes; remove them, or run--uninstallfrom their checkout, first. - The repository is
CODING_AGENT_KIT_REPO_URLwhen set, otherwise the one the managed root was cloned from, otherwise this repository on GitHub. - Only one run at a time holds the lock (
flockon.lockthroughpython3); another run exits 75 and changes nothing. Where the file system has noflock, as some NFS and Lustre mounts, the lock is the directory.lock.d, renamed into place with its owner (host, pid, time) and removed at exit; one left behind counts as gone when its pid is gone on that host or it is older than an hour. A managed root such a mount shares between hosts is safe from concurrent runs either way. - Every git call that can reach the network gives up when a transfer stays
below 1000 bytes/s for 60 s, ssh after 30 s without a connection or a
minute without an answer (unless
GIT_SSH_COMMAND,GIT_SSHorcore.sshCommandsays how to run ssh), and, wheretimeoutexists, after 300 s in all, so a stalled repository fails the run instead of keeping the lock. - The remembered prefix must be an absolute directory; a run refuses
another one. After taking the lock, a run removes the temporary links a
killed run left beside its destinations, and it writes the remembered
channel and prefix before it renames
current. --uninstallremoves the managed links and reports any other path it keeps, then~/.claude/coding-agent-kit/if it is empty;--purgealso removes the managed root.- Every child gets
/dev/nullas stdin and all work starts on the last line ofinstall.sh, so a truncated download runs nothing. - Exit 0 on success,
already currentincluded, 1 on a refusal or error, 2 on a usage error, and 75 when another run holds the lock. Nothing is changed by a run that fails; a failed first install may leave the managed root with its clone and exported release, which the next run reuses.
install.ps1 keeps its managed root in $env:LOCALAPPDATA\coding-agent-kit\,
with the same repo.git, releases\vX.Y.Z\, channel and .lock, and
two differences: current is a file that names the current release, and
install.ps1 there is a copy of that release's own installer, the entry
point for later runs. It copies the three helpers into
$HOME\.claude\coding-agent-kit\ (USERPROFILE, as Node's os.homedir()
reads it), each by a rename in that directory. A destination there is its
own when it is missing or a plain file equal to the helper of a release in
the managed root; a link, a directory or a file of other content is refused
and kept. It stages all copies first, then renames them one after the other,
each again for 1.5 s while another process (Node, a scanner) has the file
open. It takes -Channel MAJOR.MINOR, -Update, -Reinstall, -Uninstall
and -Purge, also spelled --channel, --update, --reinstall,
--uninstall and --purge, has the same channel, update, reinstall, git
transfer and exit-code rules, and holds the lock as an exclusive open of
.lock: only a sharing or lock violation that lasts a second is another
run, exit 75.
git clone https://github.com/FridrichMethod/coding-agent-kit.git
cd coding-agent-kit
./install.sh # or: sh install.sh, ./install.sh --prefix DIRinstall.sh [--prefix DIR] [--uninstall] run from a checkout creates
absolute links into the checkout; DIR must be absolute and defaults to
~/.local:
| Link | Target |
|---|---|
DIR/bin/codex-as, DIR/bin/claude-as |
bin/ |
${BASH_COMPLETION_USER_DIR:-${XDG_DATA_HOME:-~/.local/share}/bash-completion}/completions/{codex-as,claude-as} |
completions/bash/ |
DIR/share/zsh/site-functions/{_codex-as,_claude-as} |
completions/zsh/ |
- It checks every destination first. A path is this checkout's own only when
it is a symlink whose text starts with the checkout's physical path (as
pwd -Pprints it) and has no.or..components. That is the forminstall.shwrites, also when you run it through another spelling of the path. Any other existing path is refused with its name and what it is: a file, a directory, a link into another checkout, a dangling link, every relative link, whether Stow made it or you did, and a link you made through another spelling of the checkout's path, such as a symlinked directory above it or macOS/varfor/private/var. Such a link reaches this checkout but is not its own; remove it by hand. - A destination is refused too when a directory on its way, below
$HOMEor below the parent ofDIR, is a relative symlink. That is the form Stow folding writes: a missing~/.local/binbecomes a link into another repository's package, and a new link there would land in that repository. Absolute directory links are accepted, and so is any link above both, such as macOS/var;$HOMEitself and the links above it are accepted for everyDIR. Stow with--no-folding, or create such a directory (for example~/.local/bin) before you stow. - A destination whose nearest existing parent is not a writable directory is refused too, naming that parent. A refusal exits 1, and nothing changes.
- Otherwise it creates or replaces the links, so a second run is a no-op.
git pullneeds no reinstall. --uninstallremoves only this checkout's own links and reports any other path it keeps; directories stay. It first refuses, exit 1 with nothing changed, when an own link is in a directory it cannot write, or when a directory on the way cannot be searched, so it cannot tell what is there. Give it the same--prefixand environment as the install.- A link of this checkout whose target has moved or gone within it, as after
a release that moved a file, is still its own: a reinstall replaces it and
--uninstallremoves it. A moved or deleted checkout leaves dangling links that count as foreign: remove them by hand, or run--uninstallfrom the old place first. - Exit 0 on success, 1 on a refusal or error, 2 on a usage error. It never writes a configuration file and starts no login.
A checkout install then prints what it cannot do for you:
zsh: unless fpath has it, add this before compinit (with oh-my-zsh, before sourcing it):
fpath=("/home/u/.local/share/zsh/site-functions" $fpath)
Claude Code helpers, as commands in settings.json:
statusLine: node "/home/u/src/coding-agent-kit/helpers/statusline.cjs"
Notification hook: node "/home/u/src/coding-agent-kit/helpers/notify.cjs"
PreToolUse hook (Bash): node "/home/u/src/coding-agent-kit/helpers/check-git-hooks.cjs"
PATH: add /home/u/.local/bin
The PATH line appears only when DIR/bin is not on PATH. In Claude
Code's settings.json, the helpers go in as:
{
"statusLine": {
"type": "command",
"command": "node \"/home/u/src/coding-agent-kit/helpers/statusline.cjs\""
},
"hooks": {
"Notification": [
{
"matcher": "permission_prompt|idle_prompt",
"hooks": [{ "type": "command", "command": "node \"/home/u/src/coding-agent-kit/helpers/notify.cjs\"" }]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "node \"/home/u/src/coding-agent-kit/helpers/check-git-hooks.cjs\"" }]
}
]
}
}A settings.json shared by machines whose home directories differ can name
the helpers relative to the home directory instead: a managed install puts
them in ~/.claude/coding-agent-kit/ (or copy or link helpers/*.cjs there
yourself), and each command takes this form, with notify.cjs and
check-git-hooks.cjs for the two hooks; a managed install prints all three
when it places a link:
node -e "require(require('os').homedir() + '/.claude/coding-agent-kit/statusline.cjs').main()"
Each helper exports main, which does what running the file does.
Instead of install.sh you can link or copy the files yourself. A launcher
follows its own symlinks (up to 40) to bin/.. and runs lib/<tool>/launch.py
there, and bin/.. has the shape of a ~/.local prefix: link bin/codex-as
and bin/claude-as from any directory on PATH, or copy them and
lib/{codex-as,claude-as,coding-agent-kit}/ into PREFIX/bin and
PREFIX/lib. Put the completions wherever your shell loads them; they call the
launchers by name. A launcher without its lib/ reports
incomplete install at ROOT (missing lib/<tool>/launch.py) and exits 1, and
one without docs/ beside its bin/ points --help at the GitHub copy of
docs/account-launchers.md.
codex-as work # Codex as account "work"
codex-as work resume --last
codex-as work exec --json "Check the tests"
claude-as personal -p "Check the tests"
codex-as --list-accounts # account names, what completion offers
codex-as --account-info work # last-seen email, plan and usage (offline)
claude-as --set-label personal "me@example.com"
claude-as --show-config # configuration in effect
codex-as --help<codex_home> and <secrets_dir> below are configurable and default to
~/.codex and ~/.secrets (see Configuration).
- Existing accounts launch immediately. A missing one lists the existing
accounts and asks
[y/N]; after native login in an isolated temporary configuration (Codex device login, Claudesetup-token) it saves the credentials and continues the original command. codex-askeeps a fixed account view in<codex_home>/accounts/NAME, with credentials in<secrets_dir>/codex-NAME.jsonand the configuration, sessions, history and SQLite state shared from<codex_home>. Accounts and different sessions run concurrently, and interactive resume takes sessions over from the root daemon (the desktop app's backend). It forwards arguments unchanged and refuses only what would change, revoke or bypass the account's login, daemon-only commands, session names and settings that would split the shared state. The root<codex_home>/auth.jsonstays native;codex-asnever reads or changes it.claude-asstarts Claude Code with theclaude setup-tokenOAuth token from<secrets_dir>/claude-NAMEand otherwise inherits the existing configuration.- Apart from
--help,--list-accounts,--account-labelsand--show-config, each accepted only alone,--account-info NAMEandclaude-as --set-label NAME [LABEL], the launchers have no options of their own. - Each launcher keeps a small metadata cache per account in
${XDG_STATE_HOME:-~/.local/state}/coding-agent-kit/accounts/: the Codex email and plan from the account's token and the last rate limits a run saw in its own rollouts, and the Claude label asked at enrolment and the last rate limits the status line helper saw.--account-info NAMEprints it offline, without opening a credential, and the zsh completions show the email or label as each account's description. Emails show in previews and transcripts; no token is ever stored there.
docs/account-launchers.md covers the account layout, adding accounts, completion, safety boundaries and manual repair; docs/codex-as.md the Codex argument rules, daemon takeover, version compatibility and the remaining differences from native Codex.
The launchers read one optional TOML file:
CODING_AGENT_KIT_CONFIG if set, otherwise
${XDG_CONFIG_HOME:-~/.config}/coding-agent-kit/config.toml. Without it they
use the defaults:
codex = "codex" # executable: a name looked up on PATH, or an absolute or ~/ path
claude = "claude"
codex_home = "~/.codex" # accounts live in codex_home/accounts
secrets_dir = "~/.secrets"The file, and the directories and symlinks on its way, must be owned by you
(or root) and not writable by others; unknown keys only warn. Choose codex_home and secrets_dir before the first
enrolment, and after moving codex_home export CODEX_HOME for plain
codex. --show-config prints the values in effect.
docs/configuration.md has every rule and message.
- Status line: shows the active model, reported effort, project and Git branch (or detached commit), context usage, and estimated session cost. For example:
Opus | xhigh | coding-agent-kit (main) | ctx 38% | ~$1.23. Missing usage/cost data shows?; cost is Claude's estimate, not an invoice. It performs only bounded Git ref reads, without scanning the worktree or contacting the network. See the status-line reference. - Notifications:
permission_promptandidle_promptevents emit Claude'sterminalSequence, using OSC 99 for Kitty, OSC 777 for Ghostty/Warp/rxvt, or OSC 9 for iTerm2/WezTerm/Windows Terminal. The terminal handles desktop delivery, including remote sessions when terminal identification is available. Enable notifications for the terminal in OS settings; unsupported terminals and non-interactive sessions may ignore them. The notice contains only the project name and a fixed status, never prompt or command contents. Other events and notification types produce nothing. - Git hook checks: a local
PreToolUsehelper replaces per-Bashnpx block-no-verify@1.1.2execution. It blocks common--no-verify, commit-n, and-c core.hooksPath=...forms without npm startup or downloads. This is a conservative text check, not a shell parser; quoted text can trigger it, and indirect bypasses are outside its scope. Successful checks do not grant permission or skip the classifier.
The launchers style only their own messages: headings cyan, questions and
warnings yellow, saves green, errors red. Native Codex and Claude output,
completion lists and the output of --list-accounts, --account-labels,
--account-info and --show-config are never styled.
CODING_AGENT_KIT_COLOR=auto(also when unset or invalid) styles a message only when its destination is a terminal, so redirected logs stay plain;alwaysandneverforce it.- A non-empty
NO_COLORorTERM=dumbturns colour off, even withalways.
./tests/run.sh # CI: ./tests/run.sh --ci
pre-commit run --all-files./tests/run.ps1 # CI: ./tests/run.ps1 -CIrun.shneeds bash, sh, git, python3 and node. Without zsh it skips the zsh cases and says so;--cirequires zsh. WithoutcodexonPATHit skips the native Codex sharing checks, and without GNU Stow the folded Stow cases ofstandalone-e2e.sh, and says so; as root it skips the read-only directory cases.run.ps1runs the Node helper suite andtests/managed-install.ps1; it needs git, node and pwsh, and-CIrequires Windows.- Every suite runs in a temporary HOME with fake CLIs and fictitious
credentials, reaches no network beyond local fixtures, and first unsets
CODING_AGENT_KIT_CONFIG,CODING_AGENT_KIT_COLOR,CODING_AGENT_KIT_USAGE_FILE,CODING_AGENT_KIT_TOKEN_ID,CODING_AGENT_KIT_CLAUDE_PID,CODING_AGENT_KIT_REPO_URL,XDG_CONFIG_HOME,XDG_STATE_HOMEandNO_COLOR. tests/standalone-e2e.shinstalls withinstall.shunder/bin/sh, bash and (when present) dash, checks its refusals and drives both launchers and their completions through the links;tests/managed-install.shandtests/managed-install.ps1build a local repository with release tags, pass it asCODING_AGENT_KIT_REPO_URL(file://) and drive the managed installs through bootstrap, update, channel switch, lock, refusals, uninstall and purge;tests/contract.shchecks the public contract (see AGENTS.md).- CI runs the Unix suite on Linux and on macOS with the system Bash 3.2, the
Node and
install.ps1suites on Windows, and every pre-commit hook on Linux. The Linux job installs GNU Stow and fails if the folded Stow cases did not run; macOS skips them. It runs on pull requests, on pushes tomainand on pushes toci/**branches, so a change can be checked on all three beforemainis fast-forwarded to it. tests/COVERAGE.md maps each source file to its tests.
Releases follow semantic versioning within 0.x. A minor release (0.2.0 to
0.3.0) may break the contract, the command-line surface or the defaults; a
patch release (0.2.0 to 0.2.1) must not. Tags are never moved and main is
never rewritten, so a fix ships as a new patch tag.
Consumers may take patch tags automatically: a managed install moves to the
newest patch of its channel on --update, and a scheduled job may move a
submodule to the newest tag of a pinned minor line. A patch is therefore
tagged only from a green main and never changes a public interface: the
linked paths, the help text, the --account-labels and --account-info
output, the managed install's commands and paths, or anything else in the
contract in AGENTS.md. Take a minor release,
a new channel, through a reviewed change. CHANGELOG.md lists every release.
MIT - see the file for the full text.