agman — agent manager. Config profiles for AI coding agents: switch between complete work/personal setups the way AWS_PROFILE switches AWS accounts. Manages Claude Code, Codex CLI, and Gemini CLI.
A profile is everything your agents are in a given context — instructions, settings, rules, skills, agents, plugins, MCP servers — kept as one config tree per tool under ~/.agman/<name>:
~/.agman/work/claude/ -> ~/.claude
~/.agman/work/claude.json -> ~/.claude.json
~/.agman/work/codex/ -> ~/.codex
~/.agman/work/gemini/ -> ~/.gemini
agman use activates a profile by symlinking each tool's config path at it — no shell setup required, and it applies to every new session on this machine, IDE extensions included. One pure-bash script, no dependencies, works on macOS (stock bash 3.2), Linux, WSL, and Git Bash.
| Tool | Config paths | Env override |
|---|---|---|
| Claude Code | ~/.claude, ~/.claude.json |
CLAUDE_CONFIG_DIR |
| Codex CLI | ~/.codex |
CODEX_HOME |
| Gemini CLI | ~/.gemini |
none — symlink switching is the only way |
Only tools you actually have are touched: a profile covers a tool when it holds config for that tool, the CLI is on your PATH, or its config path exists. Restrict the set explicitly with AGMAN_TOOLS="claude codex" if you'd rather manage a subset. Gemini CLI is the reason agman switches by symlink rather than environment variables — it has no config-dir override at all, so nothing else generalizes.
$ agman create personal # empty profile with a starter CLAUDE.md
$ agman create work --copy-current # seeded from your current configs
$ agman use work
Switched to profile 'work' (tools: claude codex gemini).
Seeded this profile with your current Claude account (no re-login needed).
Your original configs were backed up as the 'global' profile.
$ claude # every new session now uses 'work'
$ agman off # fully restore your original configsagman use <name> symlinks every managed config path at the profile (including ~/.claude.json, so user-scope MCP servers follow it):
- The first time you switch, your original configs are moved into
~/.agman/global— a normal profile namedglobal. Switch back to it anytime withagman use global, or runagman offto physically restore it and stop managing~/.claudeentirely. - The symlink doubles as the marker: a real directory means agman isn't managing that tool; a symlink into
~/.agman/means that profile is active.agman current(orls -l ~/.claude) tells you which. - agman refuses to touch a config symlink it didn't create (e.g. dotfiles managers) and skips that tool entirely rather than half-activating it; it also refuses to remove the active profile.
Selection precedence, highest first:
CLAUDE_CONFIG_DIRset in a shell — wins in that shell only, never overridden. Useagman env,agman run, oragman execfor per-shell/per-process profiles alongside the global switch.- The config symlinks set by
agman use— machine-wide for all new sessions, no shell integration needed. This is the only mechanism that works for Gemini CLI. - Nothing — stock
~/.claude, untouched behavior.
Running sessions keep the config they started with; switching affects new sessions.
Claude Code stores a resumable session inside the config directory, at
<config dir>/projects/<encoded-cwd>/<session-id>.jsonl. Swapping that directory per profile
would strand every earlier session, so agman keeps session and history state in one shared
place that all profiles symlink:
~/.agman/.state/projects/ <- session transcripts (and auto memory)
~/.agman/.state/history.jsonl <- prompt history
~/.agman/.state/todos, file-history, shell-snapshots, session-env, sessions
The practical effect: claude --resume <id> finds your session no matter which profile is
active, and a session started under one profile is resumable under another. What stays
per-profile is the persona — CLAUDE.md, settings, rules, skills, agents, plugins, MCP
servers.
Codex CLI and Gemini CLI get the same treatment, one level down:
~/.agman/.state/codex/ <- sessions (rollouts), history.jsonl, log, tmp, .tmp (plugin cache)
~/.agman/.state/gemini/ <- antigravity, antigravity-browser-profile, tmp, history
Codex rollouts stay resumable across profiles, and the multi-GB browser profile Antigravity
keeps inside ~/.gemini is stored once instead of once per profile. A profile holds
configuration, never that bulk, so agman remove cannot take it down with it.
Upgrading from an earlier agman recovers history automatically: the next agman use merges
whatever each profile is holding into shared state, file by file, and never overwrites. Since
transcripts are UUID-named, collisions effectively don't happen; if one does, your copy is
kept aside as projects.agman-conflict rather than being lost, and both agman doctor and
agman off list anything kept aside so it doesn't sit unnoticed.
agman off turns the shared links back into real directories under ~/.claude, ~/.codex and
~/.gemini, so a restored config works without agman. Set AGMAN_SHARE_STATE=0 if you would rather each profile keep
its own history — for example to keep client work strictly separate — at the cost of
cross-profile resuming.
New profiles inherit your current Claude account, so switching never drops you at a login prompt. This needs care because Claude Code splits auth across two places:
- Account identity lives in
~/.claude.json. Claude Code runs first-launch onboarding whenhasCompletedOnboardingis absent, and treats auth as cached whenoauthAccountis set.agman createseeds those keys (plususerIDandmachineID) from your live config, andagman usebackfills any profile that predates this behavior. - Credentials live in the macOS Keychain, or in
<config dir>/.credentials.jsonon Linux and Windows. On macOS the symlink keeps the path~/.claude, so the Keychain entry is shared automatically. On Linux and Windows, agman promotes the credential file to~/.agman/.credentials.jsonand symlinks each profile at it, so a token refresh in one profile is visible to all of them.agman offturns that link back into a real file, so your restored config works without agman.
Only account identity is copied — personal project state, history, and caches stay out of
new profiles. A clean structured merge uses jq or python3 when available; with neither
installed agman copies the whole state file instead and says so, rather than silently
breaking your login.
Run agman doctor to see per-profile auth state. It reports presence only and never prints
token material.
To have a profile authenticate as a different Claude account — a work login separate from your personal one:
agman login work # runs 'claude setup-token', then paste the token (hidden input)
agman logout work # back to your shared accountThis stores a long-lived token (claude setup-token, valid about a year and it does not
rotate) at mode 600 inside the profile, and points the profile's settings.json at a small
helper script through Claude Code's apiKeyHelper setting. Because that setting applies to
the CLI, the VS Code extension, and the Agent SDK, the profile's account follows you into
IDEs — which an environment variable could not do.
Deliberately not used: snapshotting Keychain entries or .credentials.json. Refresh tokens
rotate on every use, so a snapshot is already stale by the time you switch back, and
restoring it logs you out.
Limits of setup-token credentials, worth knowing before you rely on them:
- requires a Pro, Max, Team, or Enterprise plan
- model requests only: no Remote Control and no claude.ai connectors (locally configured MCP servers still work)
- managed setups that pin
forceLoginOrgUUIDreject environment credentials, so this path may be blocked on company-managed machines - expires after about a year; rerun
agman loginto replace it
Cloning a profile never copies its account: the token, helper, and apiKeyHelper setting are
stripped, so the clone starts on your shared login.
A repository can name the profile it wants in a .agman file at its root:
profile: acme-work
Then agman use and agman run with no argument read it, from that directory or any
subdirectory:
cd ~/work/acme && agman use # activates acme-workBecause the file arrives with a clone, it is untrusted input: agman asks once before honouring it and records the decision. In a non-interactive shell it refuses rather than switching silently.
With the shell integration installed, entering the directory switches that terminal automatically and leaving it switches back:
eval "$(agman init zsh)" # or: init bashThat auto-switch works by exporting CLAUDE_CONFIG_DIR, so it is terminal-only — IDE
extensions don't inherit a shell's environment and keep following agman use. A
CLAUDE_CONFIG_DIR you set by hand always wins; the hook only manages the value it set.
Note that Claude Code already scopes plenty of things per project natively — ./CLAUDE.md,
./.claude/settings.json, ./.claude/rules/, ./.claude/skills/, ./.claude/agents/, and
./.mcp.json. Reach for a .agman file when you need to change something with no project
scope at all: the account, user-scope MCP servers, installed plugins, or the whole persona.
Homebrew (macOS and Linux):
brew install memandip/agman/agmanThat first install also trusts the tap, so from then on the short name works for everything:
brew install agman
brew upgrade agmanTo use the short name from the start, trust the tap explicitly first — Homebrew will not resolve a bare formula name from an untrusted third-party tap:
brew tap memandip/agman
brew trust memandip/agman
brew install agmanFrom a clone:
git clone https://github.com/memandip/agman.git && cd agman && ./install.shOr piped (no clone):
curl -fsSL https://raw.githubusercontent.com/memandip/agman/main/install.sh | bashUpdate later with:
agman update(update does a git pull when agman runs from a checkout, otherwise downloads the latest release script and swaps it in atomically after validation.)
| Command | What it does |
|---|---|
create <name> |
New empty profile with a starter CLAUDE.md, inheriting your current account. Flags: --copy-current (seed from current config; skips sessions/cache/history), --from <profile>, --link-plugins |
use [name] |
Activate: symlink each tool's config path → profile. With no name, reads the nearest .agman file. First use backs up your originals as the global profile, and migrates older profiles to the current layout |
off |
Deactivate and physically restore your original configs |
current |
Show the active profile and which tools it covers |
list |
List profiles; (active: …) shows the live tools |
dir <name> / env <name> |
Print a profile's Claude config dir / export lines for every tool that supports one. dir --for-cwd resolves the nearest trusted .agman file |
run [name] [-- args] |
Launch claude once under a profile without switching globally. With no name, reads the nearest .agman file |
exec <name> -- <cmd> |
Run any command with CLAUDE_CONFIG_DIR and CODEX_HOME set |
rename <old> <new> / remove <name> |
Manage profiles (rename repoints active symlinks; remove refuses if active) |
migrate |
Move profiles to the current layout and relink (runs automatically on use) |
login <name> / logout <name> |
Give a profile its own Claude account, or return it to your shared login |
cloud <subcommand> |
Sync profiles with an agman cloud server: login (URL defaults to the public server at https://agman.vercel.app), push [name] [--dry-run], pull <name> [--as <local>] [--force], init <org-slug> (new profile from a team default), list, status, logout. Pushes are versioned and encrypted at rest server-side. A synced profile is a persona, not a login: account material (the identity file, .credentials.json, per-profile tokens, codex/gemini auth, any .env) and ephemeral state (sessions, history, caches, tmp, logs — including state other products drop into the managed trees, like Antigravity's browser profile inside ~/.gemini) stay local by default and are stripped on pull; a push is refused if a synced file looks like it holds a secret. push --dry-run lists every file a push would carry with its size, without contacting the server; a payload over the server limit is refused before upload with the largest files named. The limit is read from the server when it advertises one (maxPushBytes for direct requests — hosting platforms like Vercel cap function bodies at ~4.5 MB — and blobPushBytes for the presigned blob-upload flow, which carries archives up to agman-cloud's 25 MB maximum straight to blob storage, bypassing the function body limit); pushes pick the right path automatically. AGMAN_CLOUD_MAX_PUSH_BYTES overrides and forces the direct path. Interactive pushes show curl's transfer bar during the upload (auto-detected on a terminal; AGMAN_PROGRESS=1/0 forces/suppresses it). --include-secrets (push) / --with-secrets (pull) opt into carrying account material for your own machine-to-machine restore. Pulls treat a downloaded profile as untrusted — they strip hooks/apiKeyHelper from settings.json, never touch shared-state symlinks, back up an existing profile first, and refuse the active profile without --force. Reinstallable plugin artifacts (plugin caches, marketplace clones, catalog data) are never synced in either direction; pull reinstalls them into the pulled profile from its own references (settings.json enabledPlugins + plugins/known_marketplaces.json) via the claude CLI — set AGMAN_CLOUD_NO_INSTALL=1 to skip. Dependency artifacts (.venv/venv, node_modules, __pycache__, .git — e.g. under a cloned MCP server in ~/.claude/mcp-servers) and agman's shared-state merge leftovers (*.agman-conflict, *.agman-orphaned) are never synced either: the sources next to them sync, and the artifacts are rebuilt on the target machine (pip install / npm install / git clone). Locally cloned profiles keep them — the exclusion is cloud-only. https is required (override with AGMAN_CLOUD_INSECURE=1); the API key is passed to curl via a mode-600 file, never the command line. agman cloud --verbose <sub> (or AGMAN_VERBOSE=1) logs the request line, push payload size, and response code |
update |
Self-update from git or GitHub |
doctor |
Diagnose setup: tool detection, per-path link state, per-profile auth, layout version |
stealth <subcommand> |
Block a forced OTEL exporter from leaving this machine (see Stealth): claude --otel <endpoint> [--profile <name>] [-- args] launches Claude through a local mitmproxy that answers <endpoint>'s host with 403, dropping telemetry while everything else still works; stop / status manage the daemon. Needs mitmproxy (mitmdump) + lsof + curl. |
init [zsh|bash] |
Optional per-shell integration (see below) |
The global symlink switch needs no shell setup. If you also want per-shell profiles — e.g. a work terminal and a personal terminal at the same time — either eval an export line:
eval "$(agman env personal)"or add the optional hook to ~/.zshrc / ~/.bashrc, which makes agman use/off also set CLAUDE_CONFIG_DIR in the current shell:
eval "$(agman init zsh)" # or: init bash- Switching is machine-global for new sessions (that's the point). Terminals or IDE windows with running Claude sessions keep the config they launched with.
userequires symlink support; on Windows use WSL, or Git Bash with Developer Mode enabled.- Profiles share one account by default — see Accounts. Per-profile accounts are Phase 4 on the roadmap. Treat profile dirs as sensitive: they can hold credentials and MCP tokens.
agman updateand the curl installer needcurl. Everything else is bash plus standard tools;jqorpython3improves profile creation but is not required.CLAUDE_CONFIG_DIR(used by the per-shell layer) is honored by the Claude Code CLI but not officially documented (anthropics/claude-code#33430). The global symlink switch does not depend on it.
Some Claude Code team accounts inject an OpenTelemetry exporter (OTEL_EXPORTER_OTLP_ENDPOINT)
that cannot be switched off by hand. agman stealth blocks it at the network layer, so the
telemetry never leaves your machine:
agman stealth claude --otel https://otel.acme-corp.example/v1/tracesstealth claude launches your regular claude through a local mitmproxy
daemon that answers every request to <endpoint>'s host with 403, drops the OTEL POSTs, and
passes everything else through — so api.anthropic.com and the rest keep working. --otel is
required. Add -- to forward extra args (e.g. -- --resume), and --profile <name> to also set
CLAUDE_CONFIG_DIR to that profile's config.
agman stealth claude --otel https://otel.acme-corp.example/v1/traces --profile work -- --resume
agman stealth status # is the proxy daemon running, and blocking which host?
agman stealth stop # stop the daemonstealth needs mitmproxy's mitmdump; lsof and curl come with macOS:
brew install mitmproxyOn first run agman generates ~/.mitmproxy/mitmproxy-ca-cert.pem if it's missing, and Node
trusts it automatically through NODE_EXTRA_CA_CERTS. Optionally trust it system-wide once:
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain \
~/.mitmproxy/mitmproxy-ca-cert.pemThe daemon persists across sessions until you stealth stop it. Its state — including the small
generated mitmproxy addon (block_otel.py) — lives under ~/.agman/.stealth, so it is never
synced or cloned with a profile. The dependencies are checked only when you run stealth (like
cloud needs curl/tar); the rest of agman stays dependency-free. The port defaults to 8080
(override with AGMAN_STEALTH_PORT); the CA path is
~/.mitmproxy/mitmproxy-ca-cert.pem (override with AGMAN_STEALTH_CA).
bash tests/run.sh # sandboxed: fake HOME, stub claude binary — safe to run anywhereCI runs the suite on Ubuntu + macOS and lints with shellcheck on every push.
Profile syncing between machines— shipped asagman cloud(0.8.0), backed by the agman cloud companion service- Profile sharing over git:
push/pullto a repository you own, and a.agmanfile that fetches a profile that isn't local yet - More tools: Qwen Code and Kimi CLI adapters, once their config layouts are verified against the real CLIs
- fish shell support, tab completions
--fresh-authseeding (exclude copied credentials)
MIT