Metadata-Version: 2.5
Name: allem-cli
Version: 0.1.0
Summary: Connect a coding agent to Allem. Records what the agent does; does not block it.
Author: Allem
License: Proprietary
Keywords: allem,claude-code,codex,compliance,gemini-cli,governance
Requires-Python: >=3.11
Requires-Dist: allem>=0.1.0
Requires-Dist: httpx>=0.27.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# 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

```bash
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

1. **Finds the coding agents on this machine.** It offers only what it finds, and names the file it
   will write for each one.
2. **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.
3. **Writes `~/.allem/config.toml`**, mode `0600`, holding the endpoint, one agent key, and one agent
   identity per coding agent.
4. **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. Running `allem init` again replaces Allem's entry rather than
   adding a second one.
5. **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.
6. **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*).
7. **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 init` writes
  `hooks.json` and never rewrites your TOML — round-tripping it would drop your comments and
  formatting. If a `[hooks]` table is already there, `allem init` says so and `allem status` prints
  the exact command line to add by hand.
- If a registration does not fire on your setup, `allem status` shows 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.

1. **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.
2. **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: false` too. 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

```bash
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:

1. `~/.allem/config.toml` — what `allem init` writes
2. `./.allem/config.toml` — optional, per project
3. `ALLEM_ENDPOINT`, `ALLEM_API_KEY`, `ALLEM_AGENT_EXTERNAL_ID` — environment, wins over both

```toml
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 canonical `agent_id` and `allem init` writes it into the config, so the hook can read your
  gated-action list on the agent-key path without anyone passing `--agent-id` by hand. The flag
  survives as an override.
- **One agent key serves one agent.** If you hand `allem init` an 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.
