Mafdet CLI
@mafdet/cli is the official command line: call models, query your account,
and run a permissioned local agent that reads and edits code on your own
machine. The agent's tools execute locally — the server only returns
structured tool calls, and no server-side agent permission is widened.
Requires Node.js 20 or newer.
Install
npm install -g @mafdet/cli
Or run it without installing:
npx @mafdet/cli doctor
The interactive session
Run mafdet with no arguments in a terminal and it opens a session: choose how
to authenticate, pick a model, then work with the agent.
mafdet # read-only
mafdet --allow-write # changes are shown as a diff and confirmed first
mafdet --allow-command "pnpm test"
It is the same agent as mafdet agent, drawn differently - the same tools, the
same approvals, the same cost accounting. The flags mean exactly what they mean
there, and a session given neither has no write or command tool loaded at all.
In a pipeline, under --json, in CI, with TERM=dumb, or with --no-tui,
mafdet prints usage to stderr and exits 2, exactly as before. Nothing that
reads the CLI's output will start receiving a drawing.
Quickstart
export MAFDET_API_KEY="sk-mafdet-live-..."
export MAFDET_MODEL="deepseek-v4-flash"
mafdet doctor # key, endpoints, model, balance, workspace, git
mafdet models list # what this key can actually call
mafdet chat "Explain TypeScript decorators"
mafdet account balance
mafdet doctor is the first thing to run when something is wrong. It checks
Node, both endpoints, the key, the workspace, git, both API planes, the
selected model, its tool-calling status and the balance — and only fails on a
genuine blocker, so a missing optional scope is a warning rather than an error.
PowerShell
$env:MAFDET_API_KEY = "sk-mafdet-live-..."
$env:MAFDET_MODEL = "deepseek-v4-flash"
mafdet doctor
Everything the CLI prints is ASCII, so output stays readable on a console
running a non-UTF-8 code page (the Windows PowerShell 5.1 default). PowerShell
pipes UTF-16 by default, so pass a prompt as an argument or use
--input <file> rather than piping it.
Commands
| Command | What it does |
|---|---|
mafdet doctor | One pass over key, endpoints, model, tool-calling, balance, workspace, git |
mafdet models list [--surface chat|embeddings|speech] [--agent] | Models this key can call, joined with catalogue metadata |
mafdet models show <id> | One model in detail, including whether the agent may drive it |
mafdet chat [prompt] [--model] [--system <file>] [--input <file>] [--save] | Send a prompt; streams by default |
mafdet agent "<goal>" | Local agent; read-only unless told otherwise |
mafdet agent | Interactive session (needs a terminal) |
mafdet embed [text|<file>] --model <id> [--output <file>] | Text to vector |
mafdet speech [text|<file>] --model <id> --output <file> --voice | Text to an audio file |
mafdet account balance|usage|subscription | Wallet, spend and plan (needs the account:read scope) |
mafdet sessions list|show|usage|delete | Conversations saved on this computer (never calls a model) |
mafdet sessions usage <id> --refresh | Show settled ledger figures alongside the local estimate |
mafdet config list | The resolved settings and where each came from |
--json emits one complete JSON document; --plain emits tab-separated rows
with no header, for cut and awk.
The agent's permission model
Read-only by default: list, search, read files, and git status /
git diff. It cannot change anything.
--allow-write adds edit_file and create_file, and then:
- every change is shown as a diff and confirmed first, with Enter meaning no;
- a non-interactive run cannot write at all, even with the flag — the write tools are not offered to the model in the first place;
- there is no flag that approves everything in advance.
Approving a command means allowing that program to run on your machine, as
you. The workspace bounds which files the agent's own tools may touch; it is
not a sandbox and it does not bound the program. A short list of obviously
destructive names is refused (sudo, su, doas, chown, shutdown,
reboot, and invoking a program by path), but treat that as a guard against
slips rather than as containment.
--allow-command "<prefix>" adds run_command, unprompted for exactly those
prefixes: pnpm test permits pnpm test --filter web but never
pnpm publish. There is no shell — the command is split into argv, so
pipes, redirects, ;, &&, backticks and $( ) are refused with an
explanation rather than sanitised.
Files that look like secrets — .env*, private keys, id_rsa*, kubeconfig,
credentials*, .npmrc, and all of .ssh, .aws, .gnupg, .docker,
.kube — are refused, with no override flag. The workspace boundary is decided
on real paths, so a symlink cannot lead outside it.
In an interactive session: /model, /permissions, /diff, /cost,
/compact, /exit.
Which models can drive the agent
Only models whose tool calling has been verified end to end. Check the AGENT
column:
mafdet models list --agent
A model without it is refused rather than attempted, so you do not pay for a
run that breaks halfway. mafdet models show <id> says which case a model is
in.
Credentials
The key is read from MAFDET_API_KEY. There is deliberately no --api-key
flag — it would put the key into shell history and the process list. Keys are
never written to a config file and never printed in full, only as a fingerprint
like sk-mafdet-live-<redacted>.
A project may carry a .mafdet.json with model — and nothing else.
Credentials and endpoints in a project file
are rejected outright: cloning a repository must never change where your key is
sent.
For mafdet account ... the key needs the account:read scope (create one in
API Keys). Without it the other commands still work.
Exit codes
Scripts may branch on these.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General failure (including doctor reporting a failed check) |
| 2 | Bad command line or configuration |
| 3 | Authentication or permission failure |
| 4 | Model or API unavailable |
| 5 | Local permission denied |
| 6 | Cancelled by the user |
| 7 | Cost, time, or loop ceiling reached |
Results and machine-readable JSON go to stdout; progress, warnings, summaries
and approval prompts go to stderr. So mafdet chat ... > answer.txt captures
the answer alone, and mafdet models list --json | jq always parses.
Saved conversations
Interactive sessions are saved on this computer, and only on this computer. They are not uploaded, and Mafdet holds no copy of them.
That is a statement about the transcript, not about the model. The messages you send are still sent to the API to be answered and still appear in your usage records; what stays local is the saved copy.
| Platform | Path |
|---|---|
| Linux / WSL | ${XDG_STATE_HOME:-~/.local/state}/mafdet/sessions |
| macOS | ~/Library/Application Support/mafdet/sessions |
| Windows | %LOCALAPPDATA%\mafdet\sessions |
The files are plain text. A transcript holds your messages, the model's
replies, and any source the agent read while working, so treat a session
directory the way you would treat the project it came from. API keys are the
exception: everything passes through redaction on the way to disk, so a key
pasted into a prompt is stored as <redacted>.
The agent cannot read these directories, even when you run mafdet from your
home directory and the store is technically inside the workspace.
mafdet --no-history # this run is not recorded at all
mafdet chat "..." --save # a one-shot chat is saved only when asked
mafdet sessions list # what is saved, and the path when nothing is
mafdet sessions delete <id> --yes
A single mafdet chat is not a conversation anyone resumes, so it is not
recorded unless --save is given. --no-history still wins over --save, and
a chat cancelled with Ctrl+C is not saved at all.
Deleting removes the local messages and that session's usage mapping. It does not remove API usage, billing records or charges — those belong to the API key, not to the transcript. It is also an ordinary file delete rather than a secure erase: backups, snapshots and the filesystem itself may still hold copies.
Reconciling what a session cost
mafdet sessions usage <id> totals a conversation from the local record. Those
figures are estimates, computed with the gateway's own arithmetic but computed
on your machine.
--refresh asks the billing ledger for settled figures. It pages through the
current API key's usage events for the session's time window; request IDs stay
local and are used only to match returned rows. No messages, title, workspace
or session ID are sent. Requests paid for by a different API key are left
alone and reported, so you know to run it again with that key.
A request the ledger has not settled stays pending, and a request that never
returned an ID stays unknown. Neither is shown as 0, which would read as
"it was free".
Cost
chat prints tokens and an estimated charge to stderr, computed with the same
arithmetic the gateway bills with. The wallet remains the authority — see
Usage & billing.
Cancelling with Ctrl+C closes the connection. In testing against production a
cancelled stream produced no charge, but that is an observation, not a
guarantee: if the upstream has already finished generating, a charge can still
land. The summary after a cancel is labelled partial: for that reason.
Known limitations
These are stated rather than left to be discovered:
- macOS and Windows are not verified. Linux, WSL, tmux, SSH and SSH over tmux were checked on real terminals, on Node 20 and 22.
- On Windows, the command timeout is written but unverified. It uses
taskkill /Twhere POSIX signals the process group; the shape of that call is tested, but it has never run on a real Windows machine, so whether every grandchild is reached there is unknown. Affects--allow-commandonly. /compacttruncates the conversation rather than summarising it. A long run also compacts itself near the context window, dropping the oldest tool exchanges from what is sent — your saved transcript keeps them.- The compatibility promise for
--jsonshapes and exit codes becomes binding at 1.0.0; being 0.x, semver still permits a break in a minor version.
Next steps
- MCP Server — read-only account tools for MCP clients
- Permissions & model scope
- Error reference