allem-cli — connect your coding agent to Allem
Allem records what your coding agent does. It blocks only the actions you have explicitly marked as hard gates in your rules.
That sentence is the product. Everything below is detail.
allem init installs a hook into your coding agent's own configuration. From then on, every tool
call it makes — shell commands, file edits, file reads — becomes an event in your organization's
tamper-evident audit chain, with secrets redacted before anything leaves your machine.
Install
pip install allem-cli && allem init --key alm_sk_live_...
Your key comes from the Allem console: Get Started → Connect your agent, or from the page you
land on when you confirm your email after signing up. Without --key, allem init asks for one.
That key is not what ends up on your machine. It is used once, to give each of your coding agents
its own key, and is never saved to disk. See What allem init does below.
Python 3.11 or newer. macOS and Linux.
What allem init does
- Finds the coding agents on this machine. It offers only what it finds, and names the file it will write for each one.
- Exchanges your key for one key per coding agent, before writing anything at all. The key you pasted is held in memory for the length of one request and is never written to disk. See The key on your machine below for why this matters.
- Writes
~/.allem/config.toml, mode0600, holding the endpoint, one agent key, and one agent identity per coding agent. - Registers the hook in each agent's config. It reads the file, merges its entry, and writes it
back — the original is copied to
<file>.allem-backup-<timestamp>first. Anything already in there is left exactly as it was. Runningallem initagain replaces Allem's entry rather than adding a second one. - Verifies. It sends one real event with the agent key and tells you whether it was accepted, in words: accepted, key refused, endpoint unreachable, or agent unknown.
- Reads your gated-action list so the first tool call of your first session is already checked against your own rules (see What gets checked).
- Prints where to look in the console.
If step 2 cannot be completed, allem init stops and tells you what is missing. It writes nothing
— in particular it does not save the key you gave it. There is no fallback, because the fallback
would be the exact thing this is here to stop.
The key on your machine
Allem has two kinds of key, and they do not look different:
| An organization key | An agent key | |
|---|---|---|
| Send events, ask for checks | ✅ | ✅ for its one agent |
| Read every event in your account | ✅ | ❌ |
| Create and delete agents | ✅ | ❌ |
| Change the rules your agents are judged against | ✅ | ❌ |
The key you paste into allem init is an organization key. Leaving one on a developer's laptop means
the machine being watched holds the credential that edits the rules it is watched against — so
allem init trades it for agent keys and keeps only those. What is left on your machine can send
events for your agents and ask for checks. That is all it can do.
If you already have an agent key, pass that instead: allem init recognises it, uses it directly, and
does not exchange anything.
Options
| Flag | What it does |
|---|---|
--key |
Your Allem API key. Prompted for if omitted. Held in memory only; never written to disk. |
--endpoint |
Allem API base URL. Defaults to https://api.allem.ai. |
--agent |
Your identity on this machine. Defaults to user-hostname. |
--agent-id |
Override your agent's canonical id. Normally unnecessary — the exchange returns it. |
--only |
claude-code, codex, gemini-cli — comma-separated. Skips detection. |
--scope project |
Configure this project only, not the whole user account. |
--production-url-pattern |
Regex for URLs that mean production, for the built-in floor. |
--no-verify |
Skip the test event. |
-y, --yes |
Don't ask for confirmation. |
There is no flag for the connection tier, and that is deliberate — see How your connection is recorded.
allem provision — a key for an integration you wire yourself
allem init is for coding agents. If you are wiring the Allem SDK, the MCP server or a proxy, this
mints the key for it and records which one it is:
allem provision --agent billing-bot --for sdk
It asks for your organization key (or takes --key), trades it for one agent key, and prints that
key once. It writes nothing to disk — not the key you pasted, and not the key it minted. Put it
wherever your integration reads its credentials from.
--for is required and there is no default. Allem records the answer as a statement about how this
agent is connected, and it appears in your evidence export; a guessed default would end up there too.
| Flag | What it does |
|---|---|
--agent |
The agent's id. Created if it does not exist yet. Required. |
--for |
sdk, mcp or proxy. Required. |
--key |
Your Allem organization key. Prompted for if omitted. Never written anywhere. |
--endpoint |
Allem API base URL. Defaults to https://api.allem.ai. |
--key-name |
Name for the new key in your console's key list. |
If Allem already shows an agent as "connected over the API, connector not identified", this command is what declares a connector for it. That state is not an error — it means nothing declared itself, so Allem records that it cannot tell — but it is the one thing on that screen you can change.
What it does not do is erase the old record, and that is deliberate. An agent that has been sending on an undeclared key has genuinely connected both ways, so after this it reads "Connected more than one way" and names both, with the date each was last seen. Allem never collapses that to the newer connection: events that arrived with nothing identifying them are not retroactively covered by a declaration made afterwards. New traffic from the declared key is recorded as that connector from the first event.
What each supported agent needs
| Coding agent | File written | Events used | Status |
|---|---|---|---|
| Claude Code | ~/.claude/settings.json |
PreToolUse, PostToolUse |
Verified. Allem has run on this shape against its own repo for weeks. |
| Codex | ~/.codex/hooks.json |
PreToolUse, PostToolUse |
Written from Codex's documented event names. Not yet exercised against a live install. |
| Gemini CLI | ~/.gemini/settings.json |
BeforeTool, AfterTool |
Written from Gemini CLI's documented event names. Not yet exercised against a live install. |
Two things follow from that last column, and we would rather say them than have you discover them:
- Codex also supports a
[hooks]table in~/.codex/config.toml.allem initwriteshooks.jsonand never rewrites your TOML — round-tripping it would drop your comments and formatting. If a[hooks]table is already there,allem initsays so andallem statusprints the exact command line to add by hand. - If a registration does not fire on your setup,
allem statusshows what was written where. The shape is one small adapter per agent (allem_cli/adapters.py), not an integration — corrections are cheap.
Each coding agent gets its own agent record: identity dev-laptop plus the agent's name, e.g.
dev-laptop-claude-code. Two connectors on one machine are two records, because a Claude Code
record must not silently inherit a Codex record's posture.
What is recorded
Every tool call, as an action with redacted parameters:
| The agent does | Allem records |
|---|---|
| Runs a shell command | bash.execute, git.commit, git.push_force, test.run, docker.run, net.request — with the command shape |
| Edits or writes a file | file.edit / file.write, the path, how many characters changed |
| Reads a file | file.read, the path |
| Touches a secrets path | file.access_secrets |
| Anything else | tool.<name> |
Secret values never leave your machine. Before any payload is built, five redaction rules run
over anything that would travel: environment assignments (FOO_API_KEY=…), secret CLI flags
(--token=…), authorization headers, known credential prefixes (Allem, Anthropic, GitHub, AWS,
Slack, JWT), and any 40+ character opaque token. Commands are truncated at 300 characters. What
travels is the shape of the command and the paths, not the contents of your files.
Events are written to a local spool on disk before any network attempt, so an Allem outage costs you nothing and loses nothing: the record survives and is delivered when connectivity returns.
What gets checked before it happens
Two sources, and they add — the second never narrows the first.
- Your own rules. The hook fetches your agent's gated-action list from Allem
(
GET /v1/agents/{id}/scopes/hard-gates) and caches it against its scope version. Any action on that list is checked before it runs. - A built-in floor: force-push, secrets paths, production URLs. Always checked, whatever your rules say.
This matters more than it sounds. If the check were driven by our idea of "risky" instead of your
rules, a rule you wrote denying file.edit under /config would appear in the console, read as
active, and never fire — because nothing would ever ask. A rule that lies is worse than no rule.
The same list decides what can be blocked: an action is stopped only if it is on this list and Allem denies it. Checking and blocking come from one source, so there is no second place for the two to disagree.
If the list cannot be read, the hook falls back to the floor alone, blocks nothing, and writes
that to ~/.allem/log/hook.log every time it happens. Checking less than you configured is
sometimes unavoidable. Doing it quietly is not.
What is and is not blocked
One thing can stop a tool call: an action you marked hard_gate in your rules, which Allem then
denies. Nothing else, ever.
Ordinary denials are recorded and proceed. That is a decision, not a missing feature. Hooks
across all three agents are, in Codex's own documentation, "a guardrail rather than a complete
enforcement boundary because Codex can often perform equivalent work through another supported tool
path." A denied rm in a shell does not prevent the same deletion through a file-edit tool.
Blocking everything your rules deny would claim more prevention than a hook can give — which is
precisely the overclaim Allem exists to criticise in other people's products. Marking a rule
hard_gate is you saying "this one is worth failing closed for", which is your call and not ours.
Two consequences worth stating plainly:
- Nothing is blocked when we cannot read your gated-action list. We do not enforce a rule we cannot see. The narrowing is written to the log every time it happens.
- An unhappy answer is not a denial. A revoked key returns
allowed: falsetoo. The hook only honours a denial when Allem returned a verdict it actually recorded, so an expired credential cannot turn into a broken editor.
When something goes missing
This is the part that decides whether Allem is a tool you keep. The only thing that may ever stop a tool call is an explicit deny from Allem for a hard-gated action. Every other failure — ours, yours, or the network's — lets the action through and is written down.
Two things make that true rather than aspirational. The hook process always exits 0: a decision
travels as JSON on stdout, which is how all three coding agents read one, so no crash, missing file
or timeout can be mistaken for "block". And the registration runs a generated guard script
(~/.allem/bin/allem-hook) whose only job is to check every path before running anything.
| What goes missing | What your coding agent sees | Where it is recorded |
|---|---|---|
~/.allem/config.toml deleted |
Nothing. Tool calls proceed, nothing is recorded. | guard.log |
| Config unreadable or malformed TOML | Nothing. Tool calls proceed. | hook.log, config_unusable |
| The hook module deleted (package uninstalled, venv rebuilt) | Nothing. Tool calls proceed. | guard.log, missing …/hook.py |
| The Python interpreter deleted | Nothing. Tool calls proceed. | guard.log |
The whole ~/.allem directory deleted |
Nothing. Tool calls proceed. | nowhere — there is nowhere left to write |
| Allem unreachable | Nothing. Tool calls proceed; events spool locally and deliver later. | hook.log |
| Key invalid or revoked | Nothing. Tool calls proceed. Never treated as a denial. | hook.log |
| Allem slow or hung | Nothing, after at most 5 seconds. | hook.log, deadline_exceeded |
| Malformed response, unparseable input, a bug in our code | Nothing. Tool calls proceed. | hook.log |
| Your gated-action list unreadable | Tool calls proceed. Nothing can be blocked. | hook.log, every time |
None of these leaves your coding agent erroring, and none of them requires you to know that deleting Allem's config is the fix.
This is written from an incident rather than from imagination. Allem's own repository ran an earlier
hook whose guard checked the interpreter and the config file but never the script it was about to
run. When that script moved, both checks passed, the shell exec'd Python against a deleted path,
Python exited 2 — and a PreToolUse hook that exits 2 means block this tool call. Every tool call
in the repository began failing. The trailing || exit 0 did not help, because exec replaces the
shell: once Python starts there is no shell left to run the || branch.
So the guard is a generated file with its dependencies in one list, not a shell chain that the next person extends incorrectly, and there is a test for every row of the table above — including the exact combination that caused it, config present and entry point missing.
What Allem can and cannot see
Allem sees the actions your coding agent takes through its own hook system, and records them. Actions taken another way are not visible to Allem.
This is a coding-agent hook connection — tier 2 of Allem's connection-tier model. Real tool
calls, incomplete coverage. It is not the same assurance as an agent wired through the Allem SDK,
where every action passes through check() before it happens, and Allem's console must never show
the two as equivalent.
How to uninstall
allem uninstall # remove the hook registrations
allem uninstall --purge # also delete ~/.allem (config, local log, spooled events)
uninstall removes exactly Allem's entries from each config file and leaves everything else — your
own hooks, your settings — as it was, backing the file up first. Without --purge your config and
local log stay where they are, and --purge refuses to run while events are still waiting to be
delivered unless you pass --yes.
Events already recorded stay in your organization's audit chain. An audit log you could erase by uninstalling the thing that wrote it would not be an audit log.
Configuration
Three values, three layers. Lowest precedence first:
~/.allem/config.toml— whatallem initwrites./.allem/config.toml— optional, per projectALLEM_ENDPOINT,ALLEM_API_KEY,ALLEM_AGENT_EXTERNAL_ID— environment, wins over both
endpoint = "https://api.allem.ai"
api_key = "alm_sk_live_..." # an AGENT key, not the one you pasted
agent_external_id = "dev-laptop-claude-code"
agent_id = "665f…"
[codex]
agent_external_id = "dev-laptop-codex"
api_key = "alm_sk_live_..." # its own agent key — one per agent
agent_id = "665f…"
What is in the file is what Allem sees. The identifier is used exactly as written — nothing is
appended at runtime. Each coding agent gets its own record, its own key and its own [section],
which allem init writes for you: an agent key is bound to one agent, so two coding agents that are
two agents need two keys. allem init --agent <id> makes them all one agent instead, and they then
share the one key.
Optional: production_url_pattern.
Everything else lives under ~/.allem/: bin/allem-hook (the guard), state/ (spool, per-session
sequence numbers, the cached gated-action list), log/hook.log and log/guard.log.
Limits worth knowing
An agent key cannot look itself up.Closed on 2026-08-10. The credential exchange returns the canonicalagent_idandallem initwrites it into the config, so the hook can read your gated-action list on the agent-key path without anyone passing--agent-idby hand. The flag survives as an override.- One agent key serves one agent. If you hand
allem initan agent key while two coding agents are installed, it refuses rather than half-working: the platform would reject every event from the second one. Run it once per coding agent with--only, or use an organization key, which gives each of them its own key in a single run. - Registration for Codex and Gemini CLI is unverified against a live install — see the table above. The deny shape for those two is written from their documented behaviour; if it is wrong, the agent ignores it and the action proceeds, which is the right way for that uncertainty to land.
- Per tool call, one process. The hook starts a Python interpreter for each tool call. Unchecked
calls exit before importing anything that touches the network, and the whole process has a hard
5-second ceiling (
ALLEM_HOOK_DEADLINE_S).
How your connection is recorded
An agent connected by a coding-agent hook must not display the same assurance as one wired through the SDK, so Allem stores how each agent is connected and shows it wherever its governance is shown. The value is never taken from anything an event says — the agent being judged does not get to choose how trustworthy its own record looks. It is stamped on the key at the moment the key is issued, using your organization key, which the agent does not hold.
| How you got the key | Recorded as |
|---|---|
allem init |
a coding-agent connection (Tier 2) |
allem provision --for sdk |
the Allem SDK (Tier 1) |
allem provision --for mcp |
the Allem MCP server (Tier 3a) |
allem provision --for proxy |
a proxy (Tier 3b) |
| A key created in the console | "connected over the API, connector not identified" |
allem init always declares a coding-agent connection and has no flag to say otherwise. It
installs a hook; a hook does not block anything while Allem is unreachable, and if this command could
be told to claim Tier 1, your coverage report would state that a hard gate held during an outage when
it did not.
Allem describes these as declarations, never as something it observed: once events start arriving, the SDK, a coding-agent hook, an MCP server and a proxy are indistinguishable on the wire.
For María — what a customer actually experiences
A developer at a company that just signed up opens the Allem console and lands on Get Started. Step one says "Connect your agent". They pick Claude Code and see a single line with their key already filled in, which they copy and paste into a terminal.
It takes about ten seconds. The command tells them what it found on their machine — "Claude Code →
/Users/them/.claude/settings.json" — asks once whether to write to it, and then reports each step
in plain words: the configuration file it wrote, that it backed up their existing settings first,
that it sent a test event and Allem accepted it, and how many of their own rules are now being
checked. It finishes by telling them exactly where to look in the console, and that they can run
allem uninstall whenever they want.
They go back to work. The next time they use Claude Code, every command it runs and every file it edits appears in Allem — with passwords and API keys stripped out before anything is sent. In the console, the Get Started step turns green and they can watch their own agent being observed, which is the first moment Allem is real to them rather than a description.
What they are told, in the terminal and in the console and in this document, is that Allem records what their coding agent does, and blocks only the actions they have explicitly marked as hard gates. Out of the box that is nothing at all: a new customer has no hard gates, so on day one Allem watches and never interrupts. The first time it stops something will be because they asked it to, on a rule they wrote.
They are also told that Allem only sees what the coding agent does through Claude Code — if the same person deletes a file by hand, Allem does not know. We say that first, before they discover it, because a customer who finds out later that a tool saw less than they assumed does not trust the parts that were true either. Nothing else about Allem works if that is not the habit.
And the thing they will never see, which took an incident to get right: if any piece of Allem goes missing from their machine — our config, our code, our server — their coding agent carries on exactly as before. It does not error, it does not stall, it does not ask them to debug us. A governance tool that breaks a developer's session because we shipped a bad path is uninstalled within the hour, and rightly. The only interruption they will ever get from us is the one they configured.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file allem_cli-0.1.0.tar.gz.
File metadata
- Download URL: allem_cli-0.1.0.tar.gz
- Upload date:
- Size: 106.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
943d3c1e2accb508b1ada0406ee36ea5ff2ca3022567bc1223708fbd66fa5006
|
|
| MD5 |
22c77b9819859bac6b08342782d12193
|
|
| BLAKE2b-256 |
af78ff2d3d84461c020970b49be2aa585d0a3d7a34b6a41f25305f36d68992c6
|
Provenance
The following attestation bundles were made for allem_cli-0.1.0.tar.gz:
Publisher:
publish.yml on Allemai/allem-platform
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
allem_cli-0.1.0.tar.gz -
Subject digest:
943d3c1e2accb508b1ada0406ee36ea5ff2ca3022567bc1223708fbd66fa5006 - Sigstore transparency entry: 2764800435
- Sigstore integration time:
-
Permalink:
Allemai/allem-platform@ccc2f5729a4884bc824f892d8fdc39e91ac4a37a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Allemai
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ccc2f5729a4884bc824f892d8fdc39e91ac4a37a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file allem_cli-0.1.0-py3-none-any.whl.
File metadata
- Download URL: allem_cli-0.1.0-py3-none-any.whl
- Upload date:
- Size: 74.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
439413c76262a540c53cea4093042ab11930a9842a109fb4b0b244a236fcb39d
|
|
| MD5 |
fb0207c2ad542890270a25df20b2b57a
|
|
| BLAKE2b-256 |
af5be4384fd3a34620e54afc3ae2165a8db90b05e4f422b29cd244dcc38c66d9
|
Provenance
The following attestation bundles were made for allem_cli-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on Allemai/allem-platform
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
allem_cli-0.1.0-py3-none-any.whl -
Subject digest:
439413c76262a540c53cea4093042ab11930a9842a109fb4b0b244a236fcb39d - Sigstore transparency entry: 2764800437
- Sigstore integration time:
-
Permalink:
Allemai/allem-platform@ccc2f5729a4884bc824f892d8fdc39e91ac4a37a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Allemai
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ccc2f5729a4884bc824f892d8fdc39e91ac4a37a -
Trigger Event:
workflow_dispatch
-
Statement type: