An AI coding agent — one Python core, three front-ends.
English · 中文文档
CluxMate is an AI coding agent that reads your codebase, plans changes, edits files, runs commands, and answers questions. A single Python core powers three interchangeable front-ends:
| Front-end | What you get |
|---|---|
| Headless CLI | One-shot prompts for scripts, CI, and automation (cluxmate -p "...") |
| Textual TUI | A full interactive terminal UI (cluxmate) |
| Electron desktop | A polished GUI that drives the same core over JSON-RPC (stdio) |
It speaks the OpenAI-compatible API, so it works with DeepSeek, Qwen, GLM, OpenAI, OpenRouter, Ollama, or any self-hosted endpoint using the same protocol — just point it at a base_url. No vendor lock-in.
- One core, three front-ends — the headless CLI, the REPL, the Textual TUI, and the Electron desktop all drive the exact same agent loop. The desktop app is a shell around that core; the Python agent remains fully usable on its own.
- No vendor lock-in — speaks any OpenAI-compatible API: DeepSeek, Qwen, GLM, OpenAI, OpenRouter, or a self-hosted endpoint. Configure multiple models and switch on the fly; provider failures never crash a turn, because timeouts, API errors, and network failures are translated into graceful, user-visible messages.
- Event-sourced sessions, fully traceable — every session is an append-only event log; the model's message history is derived from it, never stored separately. Every turn of every agent — main and subagents — is bracketed by
turn/start/turn/end, every step logsstep/start,request/header, and tool results, so the exact prompt sent at any step can be reconstructed and replayed verbatim, and context compaction rewrites a summary region without erasing the underlying events. See Sessions you can replay below. - Stable, cache-friendly context — the system prompt never changes with your session: memory, skills, and mode are injected as tagged synthetic messages, so request prefixes stay stable and prompt caches stay hot — with per-turn cache-hit and latency metrics surfaced in the UI.
- Risk-tiered permissions — every tool declares a risk level (
safe/write/dangerous/critical); four modes (plan/default/acceptEdits/yolo) plus two persistent always-allow lists (write tier + dangerous tier —delete_file, andbashper category likebash:rm/bash:python/bash:run, never whole-tool) control approval. Running code (python script.py,node app.js,npm run,./x.sh, …) isdangerous, notsafe.planmode is read-only by construction; dangerous commands prompt unless explicitly always-allowed, and critical commands (format/mkfs/dd, etc.) and sandbox escalation always prompt. - A two-layer sandbox — file write/delete tools are guarded by an in-process WriteFence (canonicalize-then-contain), and model-generated
bashcommands run inside an OS-level sandbox (Windows Low-integrity token, Linux bubblewrap, macOS Seatbelt). Sandboxing is fail-closed and onlyyolomode — the explicit opt-out — disarms it. An opt-in read denylist hides secrets (.env,*.pem,~/.ssh, …) from the model and from shell/MCP subprocesses. See Security: sandbox. - Network-access guard (SSRF) —
web_fetch/web_searchpass through an SSRF guard in every mode (includingyolo): internal/private addresses (RFC1918, loopback, link-local, cloud metadata, …) are denied by default, every redirect hop is re-validated, and DNS resolution failures close the request. Allow/block rules are configurable (~/.cluxmate/ssrf.json), managed from desktop Settings → Sandbox → Network access. See Security: network access (SSRF guard). - Network egress control (bash/MCP) — bash and MCP stdio subprocesses can have their outbound traffic locked down:
shared(unrestricted),off(kernel-level deny — bwrap--unshare-net/ Seatbeltdeny network*), orproxy(allowlist-only via a local filtering proxy). Defaultshared; Windowsoffis fail-closed for now. See Security: network egress (bash/MCP). - Checkpoints & rewind — a shadow-git repository per working directory snapshots your files before and after every turn, so you can undo any turn — session-scoped, so other sessions' edits surface as conflicts, never clobbered.
- One tree per task (git worktrees) — a session can run in its own linked git worktree on its own branch, created from the desktop sidebar or with
cluxmate worktree …, so parallel sessions stop fighting over the same files. Inside a tree the composer stops offering branch switches (the tree is its branch) and the project's branch list hides what other trees hold; deleting the session can take the tree with it. See Session worktrees. - Subagent delegation — delegate independent tasks to restricted subagents (
general-purpose, read-onlyexplore, and an evidence-gatedreviewerthat fixes nothing), or define your own in Markdown. Recursion is capped at depth 4, parallelism is bounded (4 at once, 2 of them writers), and every child gets a turn budget, a machine-readable status header, and its own replayable session log. - Code intelligence (LSP) — a read-only
lsptool exposes 10 navigation operations (go-to-definition, find-references, hover, call hierarchy, diagnostics, …) across seven language-server specs (Python, TypeScript, JavaScript, Go, Java, Rust, C/C++). Nothing is bundled: servers come from yourPATH, and after every edit the language server's error-level diagnostics ride back in the write tool's result. See Code intelligence (LSP). - Doom-loop guard — if the agent starts repeating identical tool calls, escalating advisories nudge it back on track;
MAX_TURNSremains the hard backstop. - Skills, memory & MCP — project-scoped skill packs, durable project memory (
AGENTS.md), opt-in retrieval memory, and Model Context Protocol servers (stdio / HTTP) plug into the same context pipeline. - Remote MCP with OAuth — HTTP MCP servers can authenticate with OAuth 2.0 (discovery → dynamic client registration → PKCE on a loopback callback), driven from the desktop MCP panel or
cluxmate mcp auth/logout/status. Tokens live in~/.cluxmate/mcp-auth.json, are always denied to the model's read path, and — deliberately — no tool exists that can trigger authorization. See Remote MCP servers & OAuth. - Lifecycle hooks — run your own shell commands at
UserPromptSubmit/PreToolUse/PostToolUse/Stop/SessionStart/SessionEnd/SubagentStop/PreCompact/Notification, receiving context as stdin JSON and deciding to block or inject context via stdout JSON. Hooks are your own trusted config, never sandboxed; crashes/timeouts degrade to a no-op. - Honest completion — an agent's own summary is a claim, not evidence: every reply is reconciled against the tool calls that actually ran, and an unsupported claim is bounced back once. A subagent that reports
successwhile its own audit says otherwise is downgraded topartial. See Trust, but verify. - Rich toolset, safely capped —
bash, file read/write/edit/delete,grep,list_dir,lsp,web_fetch,web_search,ask_user_question,todo_write, subagents, skills, memory updates and more; every tool's output is bounded — an oversized result keeps a head/tail preview and spills the rest to disk instead of flooding the context.
┌──────────────────────────────────────────────┐
│ Front-ends │
│ CLI ─── REPL ─── Textual TUI ─── Desktop │
└──────────────────────────────────────────────┘
│
JSON-RPC over stdio (desktop)
│
┌──────────────────────────────────────────────┐
│ Python agent core │
│ AgentLoop ── SessionLog (event-sourced) │
│ Builder ── Permissions ── Checkpoints │
│ WriteFence ── Bash/MCP sandbox │
│ LSP ── Skills ── Memory ── MCP ── Subagents │
│ Hooks ── Grants │
└──────────────────────────────────────────────┘
│
OpenAI-compatible API (httpx)
│
┌──────────────────────────────────────────────┐
│ DeepSeek · Qwen · GLM · OpenAI · any base │
└──────────────────────────────────────────────┘
The session log is the source of truth: an append-only sequence of events from which the model's message history is derived. This keeps request prefixes stable (prompt-cache friendly), enables exact replay, and survives crashes.
Everything the agent does is recorded in an append-only event log — one .jsonl file per session — and the provider message history is derived from those events, never stored separately. That single source of truth buys a lot:
- Every turn, every step — each turn is bracketed by
turn/start/turn/end; inside it, every model request logsstep/start+request/header(config, system prompt, tool schemas — only when they change), and every tool call logs its result. The exact prompt sent at any step can be reconstructed verbatim (session/contextshows it turn by turn). - Environment changes are logged too — memory updates, skill loads, and mode switches are recorded as tagged synthetic
user/messageevents (source: memory/skill/mode), and a mode or tool-schema change appends a freshrequest/headerwith reasonchange— so replay shows what the agent knew and could do at every point, not just what it said. - Subagents included — a subagent is just another agent loop with its own child
SessionLog, linked to its parent viasubagent/spawnpointers. Replaying a session replays the whole delegation tree, parents and children, in order. - Compaction without erasure — when a context grows too long, compaction rewrites one region of the log into a single summary message (one
ReplaceOp). The underlying events stay in the append-only log and the prompt-cache-friendly prefix stays intact — nothing is silently lost. - Crash-safe by construction — a torn tail is dropped on load, and an interrupted turn is durably closed with synthetic
tool/result+turn/end {interrupted}events, so a reloaded history is always a valid transcript. Undo rewinds to a turn boundary via a singletruncate.
An agent that says "done" has told you what it believes, not what happened. Before a reply is committed, CluxMate reconciles it against the tool calls that actually ran in that turn — a denied, malformed, or errored call doesn't count as evidence:
- Claims need backing — a claim that files changed requires a write tool to have run; a claim that tests or a command ran requires a
bashcall, and when onlybashran the named files are checked against the filesystem by modification time. - Bounced once, never blocked — an unsupported claim is refuted back to the model once, as a tagged synthetic message (the audit is advisory; it never hard-fails a turn), and the outcome lands in the log as
turn/end.reason.completion_audit. - It applies across delegation — the parent synthesizes its own view from the child's log rather than from the child's prose: a child whose
turn/endwas abnormal gets itssuccessdowngraded topartial, and every child report carries a machine-readable header (status, end reason, turns used, output tokens, elapsed).
The point is that "I did it" and "I believe I did it" are distinguishable — in the log, in the subagent tree, and in what the parent is told.
Permissions decide what is allowed; the enforcement boundaries below make denials stick:
① WriteFence (in-process) — guards the five file tools (write_file, search_replace, multi_edit, multi_write, delete_file). Every path is canonicalized (.. and symlinks resolved) then checked: deny-list first, containment second, before any I/O. Only the working directory, the platform temp dir, and your ~/.cluxmate/AGENTS.md are writable — and <project>/.cluxmate/ (permission config, MCP servers, skills) is always off-limits so a prompt-injected model can never edit its own permission settings.
② Bash + MCP sandbox (OS-level) — model-generated bash commands run under an OS sandbox instead of your full user, with a kernel-level backend on each platform:
- Windows: a Low-integrity token (
NO_WRITE_UP) with the workspace tree labeled low — the shell can read and reach the network, but cannot modify anything above its integrity level. - Linux: bubblewrap (
bwrap) mount namespaces — the root filesystem is read-only, only the workspace/temp dirs/granted folders are writable, and<project>/.cluxmateis re-mounted read-only. - macOS: Seatbelt (
sandbox-exec) —(allow default)with only file writes denied, the workspace/temp dirs/granted folders allowed, and<project>/.cluxmatedenied again. - Fail-closed: if sandboxing is on but no backend is available,
bashrefuses to run rather than falling back to a bare subprocess. Escape hatch:CLUXMATE_BASH_SANDBOX=off.
MCP stdio servers reuse the same sandbox (best-effort: they are your explicit config, so with no backend they fall back to running bare).
③ Read denylist (opt-in, user-global) — the fences above stop writes; reads are denied only if you ask. ~/.cluxmate/forbid-read.json ({"protect_sensitive": false, "paths": [...]}) hides files and directories from the model and from shell/MCP subprocesses: the paths you list (absolute, files or directory subtrees), plus — once you flip protect_sensitive — a built-in template covering .env, .git-credentials, .netrc, *.pem / *.key / *.p12 / *.pfx anywhere on disk, and the ~/.ssh, ~/.aws, ~/.gnupg directories. It is enforced in every mode including yolo (like the SSRF guard, it defends against credential theft rather than trusting model intent); the process-level fence (read_file / grep / list_dir) covers all three platforms, and bwrap/Seatbelt enforce the directory roots shell-side too — on Windows Low-integrity can only restrict writes, so the shell-side read deny is a documented no-op there. ~/.cluxmate/mcp-auth.json (OAuth tokens) is always denied, toggle or not.
Both write boundaries are enabled in every mode except yolo — the one explicit opt-out that disarms everything. Writable-folder grants (~/.cluxmate/sandbox-grants.json) let you whitelist extra directories. When the sandbox denies something, the model can request a one-off escalation (sandbox_permissions="danger-full-access" + a one-sentence reason) — this triggers a dangerous approval, and the grant applies to that single call only. The permission modes:
| Mode | Behavior | Sandbox |
|---|---|---|
plan |
Read-only toolset (writes aren't even registered) | Hard isolation |
default |
safe auto-approves; write / dangerous prompt |
On |
acceptEdits |
write auto-approves; dangerous still prompts |
On |
yolo |
Everything auto-approves, including dangerous |
Off (opt-out) |
web_fetch / web_search run in the agent process at normal network privileges — a prompt-injected model could be steered into fetching internal services (loopback dev servers, RFC1918 hosts, the cloud metadata endpoint at 169.254.169.254). The SSRF guard (cluxmate/tools/_ssrf.py) validates the destination before the request is made; it is a T1-class "value" constraint, in the same trust model as WriteFence — it constrains the URL the model supplies, not malicious code:
- Internal/private denied by default — the built-in deny table covers RFC1918 (
10/8,172.16/12,192.168/16), loopback, link-local, cloud metadata (169.254.0.0/16), CGNAT, multicast, and reserved ranges, plus IPv6 ULA / link-local / multicast / NAT64 — 17 networks in all, and they cannot be removed. - Enforced in every mode, including
yolo— deliberate: it defends against remote prompt-injection, not model intent. - Every redirect hop re-validated — httpx
event_hooksre-checks each hop of a redirect chain, so a redirect that tries to slip past is still blocked. - DNS checked per-IP, fail-closed — a hostname is resolved and every A/AAAA address is checked (a public name resolving to
127.0.0.1is blocked); a resolution failure is treated as unsafe and refused. allowwins over every block — rules live in~/.cluxmate/ssrf.json:{"allow": [...], "block_extra": [...]}, with entries ofhost/host:port/[ipv6]:port/ IP / CIDR. The file sits inside the WriteFence non-writable~/.cluxmate/, so the model cannot edit its own network allowlist.- Manage it from the desktop — Settings → Sandbox → Network access; changes take effect immediately (re-read on every request, no restart).
web_fetch / web_search are guarded by the SSRF guard above; this is the complementary boundary for bash and MCP stdio subprocesses, whose outbound traffic the OS sandbox otherwise leaves unrestricted. Egress is opt-in (shared by default) and lives in ~/.cluxmate/egress.json:
shared(default) — network unrestricted, exactly the pre-egress behavior.off— kernel-level network isolation: Linux bwrap adds--unshare-net(a fresh network namespace, loopback only); macOS Seatbelt adds(deny network*). Windows cannot do this with a Low-integrity token, sooffon Windows fails closed (bashrefuses to run) rather than pretending to be offline — real per-process denial is deferred to the phase-2 AppContainer sandbox.proxy— forces traffic through a local allowlist-filtering proxy (loopback-only). The allowlist is theallowlist from~/.cluxmate/ssrf.json(an empty list blocks everything, and unlisted hosts are denied). The proxy is injected viaHTTP_PROXY/HTTPS_PROXY/ALL_PROXY, so it's best-effort: only proxy-honoring clients are constrained.
Change it from desktop Settings → Sandbox → Network egress, or via the egress/config/set JSON-RPC method; a mode change rebuilds the agent so the new mode is baked into the sandbox backend. Like the other boundaries, egress is off in yolo mode.
Config under <cwd>/.cluxmate/ belongs to the repository, not to you: hooks run shell commands, mcp.json spawns subprocesses, lsp.json can install toolchains, skills/ and agents/ reach the model's context, and permissions.json can pre-authorize tools. So the first time the desktop or the TUI opens a directory that actually ships such config, it asks once, with three answers: trust and remember / trust this run only / do not trust. A headless run cannot ask — cluxmate -p "...", and cluxmate repl without a TTY, print one warning line on stderr and run with that config withheld. Upgrade effect: an existing <cwd>/.cluxmate/ therefore stops being loaded until you trust the directory — grant it with cluxmate trust add (the scripted / CI path, e.g. cd "$REPO" && cluxmate trust add .) or answer the prompt once in the desktop or the TUI.
While the directory is untrusted — "do not trust" and "not answered yet" behave the same — none of that project-level config is read, and permissions.json is not written either: a rule that could not be persisted is a rule nothing would ever read back. So the approval card does not offer a bare "always allow": for a call that could be remembered it offers "always allow & trust this project" (one click that trusts the directory and stores the rule), and for a directory you explicitly denied it offers only the deliberate "Trust this directory" step — a recorded denial is never undone by the same click that grants a tool. Global ~/.cluxmate/ config and the built-in defaults are unaffected. Untrusted is not read-only: file writes, the sandbox boundaries and the approval tiers behave exactly as before. The decision is recorded in ~/.cluxmate/trust.json (per directory; not a writable location, so the model cannot change it).
- CLI —
cluxmate trust statusshows the verdict for the current directory and what is being withheld;cluxmate trust addpre-authorizes it (for headless / CI). - Desktop — a card on first contact, a clickable "not trusted" badge in the working-directory bar (the one signal a directory that ships no config ever gets), and Settings → Project trust to review the decisions and revoke them.
- TUI — asks inline at startup (and when the working directory changes).
A session can run in its own linked git worktree instead of the main tree — one tree per task, so parallel sessions stop fighting over the same files. cluxmate worktree is the host-side mechanism (its --json output is the frozen contract the desktop calls):
cluxmate worktree info [--cwd DIR] --json # the project root of a directory
cluxmate worktree create [--cwd DIR] [--name SLUG] [--title T] [--base REF] [--branch NAME] [--allow-dirty] --json
cluxmate worktree list [--cwd DIR] --json # every worktree of the repository
cluxmate worktree remove NAME|PATH [--cwd DIR] [--force] [--keep-branch] --jsonThe tree lands at <repo>/.worktrees/<slug> on a new branch cluxmate/<slug> (slug from --name, else --title, else wt; a taken name becomes -2, -3…), and .worktrees/ goes into <git-common-dir>/info/exclude so the main tree never sees it as untracked — that ignore is a precondition, not a side effect: create writes it before the tree exists and fails with exclude-failed if git still would not ignore the container, because an unignored container is git add -A recording every checkout as an embedded repository. create refuses a main tree with uncommitted changes (--allow-dirty overrides); remove refuses a dirty worktree (--force discards it) and keeps the branch with --keep-branch. Inside the tree, <worktree>/.cluxmate/ holds runtime state only — trust, permissions, skills, MCP, hooks and AGENTS.md stay the main repository's files.
- Create — "New session in a git worktree" on a project (its header, its menu, or the group's) opens one dialog: a name (the directory and branch are derived from it), a base branch, and an optional branch name. A main tree with uncommitted changes blocks creation, and the dialog offers to commit them as
chore: WIPor stash them right there. The entry disappears only for a project confirmed not to be a repository. - See where you are — the session carries a badge with the tree's name (its branch in the tooltip) and lives in the project's own group.
- Remove — the session's context menu — and deleting the session itself — asks whether to take the tree with it (tree directory + branch + session, or session alone, with a note that the tree is then only reachable via
cluxmate worktree remove). Deleting a whole project asks the same question for every tree inside it, and reports any tree it could not remove instead of claiming success. - Safety rails — a removal is refused while another session still lives in that tree; the composer's branch pill is disabled inside a worktree and names the tree (a tree is pinned to the branch it was created on), while the project tree's dropdown hides the branches linked worktrees currently hold; and a new plain session never inherits a worktree directory — it starts in the project.
- The container is CluxMate's, not your work — the desktop never counts
.worktrees/as one of your uncommitted changes, and the "commit the changes" path refuses togit add -Aan unignored container.
Delegate independent work to child agents with restricted toolsets:
general-purpose— the full read/write/bash toolset for any sub-task.explore— read-only (read_file,grep,list_dir,web_fetch,web_search) for research.reviewer— judges work that is already done against the requirement it claims to satisfy, and returns a per-claim verdict with evidence (a test name, real command output, orfile:line) — a pass asserted without evidence is a failure, and "looks implemented" is not evidence. It holds no file-editing tools and fixes nothing: itsbashaccess exists to run the command that proves a claim.
Define your own as Markdown files with frontmatter (name, description, tools, model, max_turns, subagents) under ~/.cluxmate/agents/ and <project>/.cluxmate/agents/; the body becomes that agent's instructions.
- Depth cap 4 — the
tasktool is withheld at the cap; each subagent is a childSessionLoglinked to its parent viasubagent/spawn, and replay walks the whole delegation tree. - Bounded parallelism — at most 4 subagents run concurrently and at most 2 of them may write; a
taskcall can declarewrite_paths, and writers whose scopes overlap are serialized instead of racing. - Budgeted and reported — every child gets a turn budget, and its result comes back with a machine-readable header, with a claimed
successon an abnormal end downgraded topartial(see Trust, but verify).
Grepping for a symbol tells you where a name appears; a language server tells you what it means. A single read-only lsp tool exposes ten navigation operations — goToDefinition, goToDeclaration, goToTypeDefinition, goToImplementation, findReferences, hover, documentSymbol, workspaceSymbol, callHierarchy, diagnostics — backed by seven built-in server specs (Python/pyright, TypeScript, JavaScript, Go, Java, Rust, C/C++).
- Nothing is bundled — a server is used only if its binary is already on your
PATH; otherwise the tool returns its install command (e.g.npm i -g pyright) instead of failing. Auto-install is opt-in viaauto_installin~/.cluxmate/lsp.jsonor<project>/.cluxmate/lsp.json(deep-merged likemcp.json, with your own servers and argv), and installers never run inplanmode. - Diagnostics after every edit — the four write tools ask the language server for error-level diagnostics and return them inside the tool result, so the model sees the breakage it just caused without spending a turn discovering it. An edit never installs a toolchain, warnings are dropped on purpose, and a server that stays silent can't stall a write.
- Read-only by construction — all ten operations query; none mutate. Install commands are your config, so the model can never cause a toolchain download by itself.
- Skills — project-scoped instruction packs (
<project>/.cluxmate/skills.json) that the model can load on demand via theuse_skilltool. - Memory — a durable project memory file,
AGENTS.md, rendered as a tagged synthetic message every turn. A legacyCLAUDE.mdfile is also honored as a read-only fallback. - Retrieval memory (opt-in) —
remember/forgettools store cross-session facts as individual Markdown files; the most relevant facts are auto-recalled into each turn via lexical FTS5 scoring. Facts are scoped project (default) or global; off by default (~/.cluxmate/retrieval-memory.json). - MCP — Model Context Protocol servers (stdio or HTTP) plug their tools straight into the agent's context; servers are loaded once per working directory, and stdio servers are OS-sandboxed like
bash. Remote servers that speak OAuth are covered below.
Remote (HTTP) MCP servers can authenticate in three ways, resolved in this order:
-
Explicit
oauthinmcp.json— wins over everything else:{ "mcpServers": { "linear": { "url": "https://mcp.linear.app/mcp", "oauth": { "client_id": "…", "client_secret_env": "LINEAR_MCP_SECRET", "scopes": "read write", "callback_port": 0 } } } }Every field is optional: without
client_idthe client registers dynamically;client_secret_envholds the name of an environment variable;oauth: falseturns OAuth off explicitly;oauth: trueenables it with defaults. -
A static
Authorizationheader (orauthorization_env) — if you wrote credentials down yourself, they are used as written. -
Latent OAuth — a remote server with no static header can still use a stored token if one exists; a
401is what surfaces the login prompt (never a pre-emptive browser window).
-
Log in and out from either front-end — the desktop MCP panel shows a
needs_authbadge with a login button and an expiry time once connected; the CLI does the same headlessly:cluxmate mcp status # servers + auth state cluxmate mcp auth linear # opens the browser, waits on a loopback callback cluxmate mcp logout linear # delete stored credentials
-
Tokens stay out of reach — credentials are written
0600to~/.cluxmate/mcp-auth.json, always denied to the read tools, and a token bound to a URL is not reused if the endpoint changes. The model has no tool that can start an authorization; only your action can. -
Refresh is automatic, failure is honest — an expired token with a refresh token is refreshed silently; a rejected refresh deletes the credential (a transient failure keeps it).
needs_auth(you have an action) andfailed(read the error) are deliberately distinct states. At most one forced refresh-and-retry per call. -
OAuth is for remote servers only —
oauthon a stdio server is a config error, not something silently ignored.
Run your own shell commands at fixed points (Claude Code style). Commands receive context as stdin JSON and decide to block or inject via stdout JSON:
| Event | Fires | Can block | Can inject |
|---|---|---|---|
UserPromptSubmit |
before the prompt reaches the model | ✓ | ✓ |
PreToolUse |
before a tool runs (after approval) | ✓ (denies the tool) | ✓ |
PostToolUse |
after a tool runs | — | ✓ |
Stop |
after the reply, before it's committed | ✓ (re-runs the model, max 3) | ✓ |
SessionStart |
once per session start | ✓ (aborts startup) | ✓ (first turn only) |
SessionEnd |
clean shutdown / session switch / REPL /clear |
— (output discarded) | — |
SubagentStop |
after a subagent finishes | ✓ (replaces its reply) | ✓ |
PreCompact |
before auto-compaction | ✓ (skips it this step) | ✓ |
Notification |
turn end (desktop) + hooks/notify RPC / "Test notify" button |
— (fire-and-forget) | — |
- Block: output
{"decision":"block","reason":"..."}(or{"continue":false,...}) or exit with code 2 → the action/reply is blocked and the model receives the reason. - Inject: output
{"hookSpecificOutput":{"additionalContext":"..."}}→ extra context is injected for the model. - Location:
~/.cluxmate/settings.json(global) and<project>/.cluxmate/settings.json(project, runs after global) are merged. Event-specific payload fields (source,reason,subagent_id,trigger,message, …) ride along in the stdin JSON. - Trust model: hooks are your own trusted config, running in a normal subprocess (not sandboxed); crashes/timeouts degrade to a no-op. Edits apply via Reload in the desktop Hooks view (or restart for CLI/TUI).
- Python ≥ 3.12
- Node.js 18+ and npm (for the desktop app)
git clone https://github.com/r1c7/CluxMate.git
cd cluxmate
pip install . # direct install
# or, for development — editable install (your changes take effect immediately):
pip install -e .
pip install pytest pytest-asyncio # dev deps are not in pyproject.tomlAfter installation the cluxmate command is available on your PATH.
The desktop app drives the Python agent via cluxmate agent stdio, so install the Python package first and make sure cluxmate is on your PATH.
cd desktop
npm installThen run it in one of two ways:
-
Development mode — launches the app with hot reload, for working on the desktop code:
npm run dev
-
Build & package — compiles and produces an installer for the current platform (
dist/):npm run package # current platform npm run package:win # Windows installer npm run package:mac # macOS DMG npm run package:linux # Linux AppImage / deb
Other useful commands:
npm run build # compile only (electron-vite build)
npm run preview # preview the built app
npm run typecheck # type-check main + renderercluxmate -p "Explain the session log design" # headless one-shot
cluxmate -p "Refactor this" --model-id deepseek # with a specific model entry
cluxmate -p "..." --reasoning-effort high # reasoning level (per-dialect)
cluxmate repl # interactive REPL
cluxmate # Textual TUI
cluxmate agent stdio # JSON-RPC stdio server (desktop backend)
cluxmate mcp status # MCP servers + auth state
cluxmate mcp auth linear # OAuth login for a remote MCP server
cluxmate mcp logout linear # delete stored MCP credentials
cluxmate trust status # is this directory's .cluxmate config loaded?
cluxmate trust [list|add|deny|remove|status] [path] # project trust: inspect / grant / revoke
cluxmate worktree create --name me # linked tree <repo>/.worktrees/me on branch cluxmate/me
cluxmate worktree [info|create|list|remove] [--cwd DIR] --json # same mechanism, one line of JSON per callRun cluxmate once to seed a default config, then pick a model in the TUI/desktop Settings.
Global config lives at ~/.cluxmate/config.json (schema v2) — a list of model entries plus the active model:
{
"version": 2,
"models": [
{
"id": "deepseek",
"api_type": "openai",
"provider": "DeepSeek",
"base_url": "https://api.deepseek.com",
"api_key": "",
"model_name": "deepseek-v4-flash",
"context_1m": false,
"max_tokens": 80000,
"reasoning_efforts": ["low", "high", "max"]
}
],
"active_model_id": "deepseek"
}base_url— any OpenAI-compatible endpoint (DeepSeek, Qwen, GLM, OpenAI, OpenRouter, Ollama, self-hosted…).api_key— may be left empty; it falls back to theDEEPSEEK_API_KEY/OPENAI_API_KEYenvironment variables.context_1m— enable for providers with 1M-token contexts.max_tokens— output budget; leave empty/0for the 32768 default.reasoning_efforts— optional per-model override of the reasoning-effort levels (the system ships presets per dialect: DeepSeek / Qwen / GLM / OpenAI).
Per-project state lives under <project>/.cluxmate/ — it stays with the project, not your home directory:
| Scope | Files |
|---|---|
Project (<project>/.cluxmate/) |
permissions.json (always-allow lists), mcp.json, lsp.json, settings.json (hooks), skills/ (skill packs) + skills.json (which copies are disabled), agents/ (custom subagent types), memory/facts/ (retrieval facts), plus scratch dirs like tmp-spill/ |
Global (~/.cluxmate/) |
config.json (models), AGENTS.md (global memory), sandbox-grants.json (extra writable folders), forbid-read.json (read denylist), ssrf.json (network allow/block), egress.json (bash/MCP egress mode), trust.json (project-trust decisions), retrieval-memory.json, mcp-auth.json (OAuth tokens), checkpoints/, plus the global halves of the two-root families — mcp.json, lsp.json, settings.json, agents/, skills/, memory/global/facts/ |
Config families with a global and a project half are resolved global-then-project: mcp.json, lsp.json and custom subagent types let the project copy override the global one, while hooks are additive — global hooks run first, then the project's. The registers the model must never be able to edit — writable-folder grants, the read denylist, the SSRF rules, the egress mode, the project-trust registry — are deliberately user-global, out of a prompt-injected model's reach.
The Electron desktop is a full-featured front-end for the same Python core:
- Sessions & working directories — switch projects, resume or delete sessions, search past sessions, and give a session its own git worktree from the sidebar.
- Git integration — current branch awareness, per-turn checkpoint timeline, diff viewer, and undo/checkout across branches. Inside a worktree the composer names the tree instead of offering a branch switch.
- Destructive actions state their blast radius — deleting a session that lives in a worktree asks whether the tree and its branch go with it (and refuses while another session still uses it), deleting a project asks the same for the trees inside it, and every confirmation — worktree removal, group delete, checkpoint restore, memory-fact delete — is one dialog that carries the path, branch and uncommitted-file list as facts — with "no uncommitted files" and "could not read git" kept visibly distinct.
- Inspectors — agent inspector, context viewer (what the model saw), subagent tree, tool-call cards, permission cards, and a live task-list panel driven by the agent's own
todo_writecalls. - Views — hooks, MCP servers (with OAuth login / logout and a
needs_authbadge), skills, settings (per-model configuration including reasoning effort, sandbox folder grants, the read denylist with its sensitive-file toggle, network access rules and egress mode, memory facts, theme, font, language, notification behaviour). - Stays in your way only when it must — the taskbar icon flashes while a prompt is waiting on you (on macOS, a critical dock bounce).
- Tray app — stays in the system tray with a show/quit menu.






{ "hooks": { "PreToolUse": [ { "matcher": "bash", "hooks": [{"type": "command", "command": "python .cluxmate/hooks/audit.py", "timeout": 30}] } ] } }