Harmless is an AI coding harness that lives entirely inside Emacs. It is written in Emacs Lisp. It is not gptel, and it does not wrap Grok Build, Claude CLI, or any other external agent.
Conceptually, Harmless is similar to Claude Code, or OpenAI Codex, or Grok Build, among many other examples, but is not built upon any of those, and lives inside of your Emacs.
Several sessions can run at once in one Emacs, each pinned to a provider and model. Most people will configure a single provider (xAI, OpenAI, Anthropic, or a local OpenAI-compatible server). Mixing is optional: one session on Grok 4.6, another on a local LLM, a third on Claude, each in its own project.
Requires GNU Emacs 31.
(add-to-list 'load-path "~/programming/emacs/harmless/lisp")
(require 'harmless)Browser login is the same flow as Grok Build: Harmless opens auth.x.ai,
you sign in, and tokens are stored in auth.json under
harmless-directory (mode 0600). They refresh automatically.
(setq harmless-providers (list (harmless-make-xai))
harmless-default-model "grok-4.6"
harmless-default-reasoning-effort "xhigh")Then M-x harmless-login. That command is the shared entry for every
provider login. With more than one named connection it asks which
account. Prefix argument (C-u M-x harmless-login) uses xAI's
device-code flow, which is also used automatically if port 56121 is
already taken (for example by Grok Build itself).
Several accounts on the same vendor are separate connections: give each
a name. The default names (xAI, Anthropic, OpenAI) keep the
original token files (auth.json, auth-anthropic.json,
auth-openai.json). Extra connections get auth-<slug>.json.
(setq harmless-providers
(list (harmless-make-xai "xAI")
(harmless-make-xai "xAI work")
(harmless-make-anthropic "Anthropic")
(harmless-make-anthropic "Anthropic work")
(harmless-make-openai))
harmless-default-provider-name "xAI"
harmless-default-model "grok-4.6"
harmless-default-reasoning-effort "xhigh")Then M-x harmless-login and pick xAI work (or Anthropic work)
to sign that account in. Sessions pin to a connection name, so one
buffer can stay on personal Grok while another uses work Claude.
Anthropic / Claude uses Claude Code's OAuth client. The browser shows
a code; paste it at the minibuffer prompt. Tokens go in
auth-anthropic.json under harmless-directory. If you already ran
claude login, Harmless will reuse a still-valid
~/.claude/.credentials.json access token (it never writes or
refreshes that file).
If you already ran grok login, Harmless will reuse ~/.grok/auth.json
until you sign in or out from Emacs. It never writes that file.
An API key still works as a fallback when no OAuth session is active:
put it in ~/.authinfo (machine api.x.ai login apikey password xai-...)
or export XAI_API_KEY. Settings can live in your init file or in
harmless/config.el under user-emacs-directory (typically
~/.emacs.d/harmless/config.el).
OpenAI:
(setq harmless-providers (list (harmless-make-openai))
harmless-default-model "gpt-5.5")M-x harmless-login → OpenAI opens ChatGPT OAuth (Codex CLI's
public client, loopback on localhost:1455). Prefix argument pastes
the redirect URL instead, which is also used if that port is busy
(for example Codex CLI itself). Tokens go in auth-openai.json. A
still-valid ~/.codex/auth.json is reused (never written). ChatGPT
subscription tokens are sent to the Codex Responses API
(chatgpt.com/backend-api/codex/responses), not api.openai.com.
An OPENAI_API_KEY still works as a usage-based fallback on Chat
Completions.
Anthropic:
(setq harmless-providers (list (harmless-make-anthropic "Anthropic"))
harmless-default-model "claude-sonnet-4-6")Local OpenAI-compatible server (Ollama, llama.cpp, vLLM, …):
(setq harmless-providers
(list (harmless-make-openai-compat
"local"
:host "127.0.0.1:11434"
:protocol "http"
:endpoint "/v1/chat/completions"
:key "none"
:models '("llama3.2"))))If nothing is configured, M-x harmless walks through a short setup.
| Command | What it does |
|---|---|
M-x harmless |
Open or resume this project's session |
M-x harmless-new |
Start a new session (picks a model) |
M-x harmless-chat |
Start a chat with no project and no working directory |
M-x harmless-dashboard |
All sessions, grouped by project |
M-x harmless-switch |
Jump to a live or saved session |
M-x harmless-menu |
Transient: new / switch / model / effort / permissions / abort |
M-x harmless-info |
Open the Info manual (h in the transcript, dashboard, usage buffer, or menu) |
M-x harmless-set-reasoning-effort |
Set low / medium / high / xhigh |
Click xAI/grok-4.6 (xhigh) in the session header |
Provider, then model, then effort |
M-x harmless-login |
Sign in (xAI, Anthropic, or OpenAI) |
M-x harmless-logout |
Sign out of a provider |
M-x harmless-steer |
Queue a follow-up without aborting the turn, including during a permission question |
M-x harmless-abort |
Cancel the in-flight turn and any foreground shell. A background shell keeps running |
M-x harmless-compact |
Summarize older turns and keep the recent ones (k in the transcript). Also runs before a prompt that would fill the context window. |
M-x harmless-rewind |
Drop turns after the one at point and save the shorter transcript (w in the transcript) |
M-x harmless-edit-prompt |
Replace the prompt at point, drop what follows, and send the new text (E in the transcript) |
M-x harmless-fork |
Copy the transcript through the turn at point into a new session (f in the transcript) |
M-x harmless-rename |
Set the session title. The id stays (t in the transcript) |
M-x harmless-export-session |
Write the transcript as Markdown (x in the transcript) |
M-x harmless-delete-session |
Delete the session from disk (X in the transcript) |
M-x harmless-task-start |
Run one prompt in another Emacs that keeps going after this one exits |
M-x harmless-task-status |
Read that task's status |
M-x harmless-publish |
Publish a Markdown or HTML file as a page |
M-x harmless-team-add-seat |
Record a named seat in a rig |
M-x harmless-team-occupy |
Put a session in that seat, or replace it |
M-x harmless-team-add-work |
Append text the seat keeps when the session changes |
M-x harmless-team-seat |
Read the seat back |
M-x harmless-team-edge |
Record an edge between two seats |
M-x harmless-team-edges |
Read the edges on a rig |
M-x harmless-team-launch-order |
Read the order delegates_to and spawned_by decide |
M-x harmless-team-preview |
Show the seats, edges, and launch order a boot would record |
M-x harmless-team-boot |
Record a rig from starter, a JSON spec, or a bundle |
M-x harmless-team-grow |
Add a seat to a rig that is already recorded |
M-x harmless-team-remove |
Remove a seat. Work needs a fallback seat |
M-x harmless-team-adopt |
Bind a Harmless, Claude Code, Codex, Pi, or terminal session that is already running |
M-x harmless-team-release |
Clear that binding. The session keeps running |
M-x harmless-team-send |
Append text to a seat, a pod, or the whole rig |
M-x harmless-team-capture |
Read the text on a seat's screen |
M-x harmless-team-transcript |
Read text sent to a seat across every occupant |
M-x harmless-team-room-say |
Append a line to the rig's chatroom |
M-x harmless-team-room-history |
Read that room, or one topic |
M-x harmless-team-room-wait |
Wait until someone speaks in a topic |
M-x harmless-team-pace |
Start a paced sequence aimed at one seat |
M-x harmless-team-pace-add |
Append one piece to that sequence |
M-x harmless-team-pace-show |
Read the sequence without sending |
M-x harmless-team-pace-step |
Send the next piece, and only that piece |
M-x harmless-team-row |
Open one unit of owned work aimed at one seat |
M-x harmless-team-rows |
Read those rows, or one seat's rows |
M-x harmless-team-row-read |
Read one row by its id |
M-x harmless-team-row-claim |
Hold an open queue row |
M-x harmless-team-row-note |
Append a note to an open or held row |
M-x harmless-team-row-close |
Close a held row with a delivery reason |
M-x harmless-team-row-hand |
Hand a held row to another seat in one step |
M-x harmless-team-row-park |
Park a held row on one wake |
M-x harmless-team-row-resolve |
Record the decision and return the row to held |
M-x harmless-team-intake |
Record an observation nobody owns |
M-x harmless-team-intakes |
Read that intake, oldest first |
M-x harmless-team-board |
Read held, stalled, or waiting rows |
M-x harmless-team-workflow |
Declare steps; closing one opens the next |
M-x harmless-team-workflows |
Read those declared workflows |
M-x harmless-team-watchdog |
Record a job that stays in the rig file |
M-x harmless-team-watchdogs |
Read those jobs, oldest first |
M-x harmless-team-watchdog-check |
Read the jobs that are due |
M-x harmless-team-snapshot |
Store a named copy of the rig |
M-x harmless-team-snapshot-check |
Report each seat before a restore |
M-x harmless-team-snapshot-restore |
Write that copy back when no seat failed |
M-x harmless-team-successor |
Hand a seat to a session that is already running |
M-x harmless-team-boundaries |
Read each handover recap, oldest first |
M-x harmless-team-image |
Capture an occupied seat into its own file |
M-x harmless-team-image-start |
Record a new seat from that image |
M-x harmless-team-packet |
Capture what an occupied seat knows |
M-x harmless-team-packet-apply |
Carry that onto a session that is already running |
M-x harmless-team-compact-plan |
List seats near the context wall |
M-x harmless-team-compact |
Record a deliberate compact and tell the room |
M-x harmless-team-compacts |
Read those compact notices, oldest first |
M-x harmless-scope-project |
Write a project's intent on disk |
M-x harmless-scope-mission |
Add a mission under that project |
M-x harmless-scope-slice |
Specify one slice of a mission |
M-x harmless-scope-show |
Read intent composed down the tree |
M-x harmless-scope-proof |
Store evidence on a slice |
M-x harmless-scope-judge |
Record who accepted or rejected an item |
M-x harmless-scope-progress |
Derive proved, rejected, or open |
M-x harmless-context-list |
List context packs in a project |
M-x harmless-context-show |
Read one pack's manifest |
M-x harmless-context-get |
Read a pack, a file, or one section |
M-x harmless-context-profile |
Compose a situation and report the token total |
M-x harmless-add-directory |
Let this session read and edit a directory outside the project (+ in the transcript) |
M-x harmless-reload-all-harmless |
Reload the Lisp checkout into this Emacs |
In the prompt window: C-c C-c sends. While a turn is streaming, C-c C-c
queues that prompt and the turn keeps running. While a permission question
is showing, C-c C-c is refused. When a tool needs approval: y allow,
n deny, ! always allow that class for the rest of the session.
In the transcript, a tool run is a collapsed line. TAB toggles the
entry at point, Left closes it, Right opens it, and S-TAB toggles
every entry. A run of reads and searches is one explored line.
Permission modes: ask (default), accept-edits, always-approve,
and auto. Shell commands still prompt in accept-edits. In auto,
a reviewer allows ordinary calls and escalates the rest.
Each session is a read-only transcript buffer plus a small prompt window at
the bottom. The dashboard is a tabulated list. Config and sessions live
under harmless-directory, which defaults to harmless/ in
user-emacs-directory (so ~/.emacs.d/harmless/ for a typical setup).
The buffer is a view, not the source of truth.
Harmless reads AGENTS.md, HARMLESS.md, and .harmless/HARMLESS.md
from the session directory and from each parent directory up through
your home directory. CLAUDE.md is included too, according to
harmless-claude-md: nil never, t always, and anthropic (the
default) only when the session model id starts with claude-. Each
file applies to its directory and everything under it. Outer
directories come first. At each directory, CLAUDE.md comes first
when it is included, then AGENTS.md, then HARMLESS.md, then the
file inside .harmless/. When two files disagree, the later one wins.
A file can include another file with @path. The path is relative to
the file that contains it and must stay inside the session directory.
The text is sent with every turn and is not stored in the transcript.
Harmless reads Markdown files in .claude/rules/ in the session
directory, including files in subdirectories. The subdirectory name
does not scope the rule. A file with no paths field in its YAML
frontmatter is sent on every turn. A file with paths is sent only
when a tool call in the transcript names a file that matches one of
those globs. * stays inside one path segment. ** crosses
segments, including none. A rule in a parent directory is not read.
Nil, an empty string, a relative path, and the filesystem root are
not searched. The text is sent with the turn and is not stored in
the transcript. When two rules disagree, the later file in sorted
path order wins.
Every turn starts with a harness prompt: which tool to use, when to verify, and when to delegate. It is sent for a project chat and for a detached chat. Project instructions, path-scoped rules, and, in plan mode, the plan rules are sent after it. When they disagree with the harness prompt, they win. The prompt is not stored in the transcript.
A skill is a directory with a SKILL.md file. The file starts with
name and description frontmatter. Each turn lists the name,
description, and path. The model can still read that file with
read_file. The body is not in the catalog.
Load one with the skill tool, with M-x harmless-load-skill, or by
starting the prompt with /name. The name is a skill id, not a path.
Later turns include the body. The prompt that loaded it stays in the
transcript. Frontmatter allowed-tools limits the tools those turns
can call. A skill with no allowed-tools field does not narrow the
tools. An empty list offers no tools. A tool name Harmless does not
have is refused, and the skill that was already loaded stays loaded.
Harmless looks for skills/ inside these directories, at every level
from your home directory down to the session. A skill closer to the
session replaces one of the same name farther away. In a single
directory, later entries in this list replace earlier ones:
| Directory | Whose layout |
|---|---|
.agents/skills/, .codex/skills/ |
Codex |
.claude/skills/ |
Claude Code |
.grok/skills/ |
Grok Build |
.harmless/skills/ |
Harmless |
/etc/codex/skills is included when it exists, at the lowest priority.
Codex's own bundled skills are not on disk and are not read.
A working directory can name the skills it wants in
.harmless/loadout, one skill id per line. Blank lines are
skipped. M-x harmless-skill-loadout reports which of those names
are present in that directory, which are missing, and which
discovered skills were not declared. It reads only that directory:
vendor skills folders there, then installed plugins there. A
parent directory, /etc/codex/skills, and user plugins are not
part of this reconciliation. A prefix argument writes
.harmless/loadout.managed with the present names, one per line.
It does not copy a skill and it does not install a plugin.
A plugin is one directory of skills, agents, hooks.json, and
mcp.json. M-x harmless-plugin-install copies that directory. It
does not merge the files into .harmless/skills, .harmless/agents,
or the project's mcp.json. M-x harmless-plugin-inspect reads one
plugin directory and lists its skill ids, its hooks, and its MCP
server names. That command does not copy the directory. Installing
stays an explicit copy.
A project copy is .harmless/plugins/NAME. A user copy is
plugins/NAME under the Harmless data directory. A later plugin
name wins on a clash. A project copy wins over a user copy. Delete
that directory to remove the bundle.
A marketplace is a directory. marketplace.json lists plugins when
it is present. source is a relative path inside the marketplace.
Without that file, a child directory whose plugin.json name matches
the directory is a plugin. The name is a plugin id, not a path.
An output style is a session-wide role, tone, and format.
M-x harmless-output-style switches it. The project instruction
files are not edited.
default and an empty name clear the style. A style file is
markdown. The name is a style id, not a path. Later turns send
the body.
User styles live in output-styles under the Harmless data
directory, and in ~/.claude/output-styles. Project styles live
in .harmless/output-styles and .claude/output-styles under the
session directory only. A project style wins over a user style.
In the project, .harmless wins over .claude. The chosen text
is saved with the session. A fork keeps it.
generate_image writes an image into the project. prompt is the
description. path is the file. A relative path is resolved
against the session directory, not the buffer's default directory.
The bytes come from harmless-image-generate-function. When that
function is unset, the tool refuses. Plan mode does not generate.
A detached chat has no project to write into.
M-x harmless-task-start writes a task file and starts another
Emacs to run one prompt. That process keeps running after this
Emacs exits. M-x harmless-task-status reads the task. The file
is tasks/<id>.json under harmless-directory.
emacs --batch -l harmless -f harmless-headless-batch runs one
prompt in the current process and prints the reply. The next two
arguments are the project directory and the prompt.
M-x harmless-publish writes an HTML page from a Markdown or HTML
file in the project. Markdown headings, paragraphs, fenced code,
inline code, and links are rendered, and the rest of the text is
escaped. An HTML document is copied. An HTML fragment is wrapped
in a document. With no destination, the page is
.harmless/pages/NAME.html. A relative destination stays in the
project. The source file is left unchanged.
M-x harmless-team-add-seat records a seat in a pod of a rig.
The address is pod-member@rig. The seat starts with no
session. M-x harmless-team-occupy sets the session id, and
calling it again replaces that session. The name, the role, and
the inbound work stay. M-x harmless-team-add-work appends one
text to that work. The record is teams/<rig>.json under
harmless-directory.
M-x harmless-team-edge records an edge. delegates_to starts
the source before the target. spawned_by starts the target
before the source. can_observe, collaborates_with, and
escalates_to are recorded and do not change that order.
M-x harmless-team-launch-order reads the order. It does not
start a seat.
M-x harmless-team-preview shows the seats, the edges, and the
launch order a spec would record. It does not write. The spec
is starter, an absolute JSON file, or a directory with
rig.json. M-x harmless-team-boot records that rig. A second
boot of the same rig is refused. M-x harmless-team-grow adds a
seat. M-x harmless-team-remove shrinks the rig: it drops a
seat and the edges that touch it. A seat that still holds work
needs a fallback seat.
M-x harmless-team-adopt binds a session that is already
running. It does not start that session. The runtime is
harmless, claude, codex, pi, or terminal. A Harmless
session is an id that already has a session directory, including
one started by hand. Claude Code, Codex, and Pi take an
absolute path that already exists. A terminal takes that path
or the id of a live process. M-x harmless-team-release clears
the binding and leaves the session, the file, and the process
running.
M-x harmless-team-send appends text to one seat, a comma-separated
list of seats, a pod, the rig name, or *. It checks that the text
is on the screen afterward. It does not start a turn. A Harmless
seat gets one user line in messages.jsonl. A terminal log is
appended. Claude Code, Codex, and Pi use screen.txt in the session
directory. A live process id has no screen, and neither does a
directory with no screen.txt. The whole send is refused before any
screen changes when a seat is empty, at a prompt, or at a permission
question. A prompt is a last line > or a last line that ends in
>. Allow this tool? is a permission question. A Harmless seat
uses its status: streaming is a prompt and waiting-permission is a
permission question. M-x harmless-team-capture reads that screen.
A process id has no file, so the capture is the transcript.
M-x harmless-team-transcript reads every text sent to the seat,
including text sent to an earlier occupant. Release and removal do
not delete that history.
M-x harmless-team-room-say appends one line to the rig's chatroom.
A topic is letters and digits. The speaker is any text. The line
stays in the rig file. M-x harmless-team-room-history reads every
topic, or one topic. M-x harmless-team-room-wait blocks until a new
line arrives in that topic. Lines already stored do not count.
Nobody speaking within harmless-team-room-wait-seconds (default 10)
stops the wait. The room does not start a turn.
M-x harmless-team-pace starts a named sequence aimed at one seat.
M-x harmless-team-pace-add appends one piece.
M-x harmless-team-pace-step sends the next piece to that seat and
then moves forward. One call sends one piece. A seat at a prompt or
a permission question refuses the step, and the sequence stays put.
A finished sequence refuses another step.
M-x harmless-team-row opens one unit of owned work aimed at one
seat. The state is open. The history starts with opened.
The row stays in the rig file when that seat's session is
replaced, released, or removed. M-x harmless-team-rows reads
the rows, or one seat's rows. M-x harmless-team-row-read reads
one row by its id.
M-x harmless-team-row-claim holds an open row. The history
gains claimed. M-x harmless-team-row-note appends one note
to an open or held row. M-x harmless-team-row-close closes a
held row. The reason is handed_off_to, blocked_on,
denied, canceled, no-follow-on, or escalation. The
history gains delivered, and the reason is stored. Closing
records delivery. Acceptance stays the next seat's verdict, and
the close does not open another row.
M-x harmless-team-row-hand hands a held row to another seat.
The source row closes with handed_off_to and the destination
row opens with the same body, in one write. The destination
row stays open. Handing a row to its own seat is refused.
M-x harmless-team-row-park parks a held row on one wake. A
second wake is refused while it is parked. A summary and a
pointer to the evidence together ask a person. One without the
other is refused. M-x harmless-team-row-resolve records the
decision and returns the row to held. That is the owner's wake.
It does not start a turn.
M-x harmless-team-intake records an observation on the rig.
It has no seat. M-x harmless-team-intakes reads them, oldest
first. M-x harmless-team-board reads one view: held is a
row the owner still has, stalled is a held row with nothing
after the claim, and waiting is a park that asks a person.
M-x harmless-team-workflow declares an ordered list of steps
and opens the first one. M-x harmless-team-workflows reads
them, oldest first. Closing the current step opens the next
step's row in the same write. The owner decides that the step
is done. The next row stays open. The last close opens no
row. Handing the current step is refused.
M-x harmless-team-watchdog records a reminder, a pool of
artifacts, a nudge to a workflow, an idle queue, or a seat
near its context wall. M-x harmless-team-watchdogs reads
them. M-x harmless-team-watchdog-check reads the ones that
are due. The job is in the rig file, so it is still there
after Emacs restarts. The check does not arm a timer, a
schedule, or a monitor.
M-x harmless-team-snapshot stores a named copy of the rig in
its own file. M-x harmless-team-snapshot-check reports each
seat as resumed, rebuilt, fresh, waiting, or failed. The check
does not write. A fresh start is a choice, and the report
labels it fresh. M-x harmless-team-snapshot-restore writes
that copy back only when no seat failed.
M-x harmless-team-successor hands one seat to a session that
is already running. The seat keeps its name, its edges, and
its inbound work. The recap of decisions is written on the
rig at that boundary. M-x harmless-team-boundaries reads
those recaps, oldest first. The earlier session keeps running.
M-x harmless-team-image captures an occupied seat into its
own file. M-x harmless-team-image-start records another
seat from that file. The new seat copies the role and the
inbound work, and it starts with no session.
M-x harmless-team-packet captures what that seat knows.
M-x harmless-team-packet-apply puts that work back and binds
a session that is already running. The runtime must change.
The seat keeps its name. Neither command starts a process.
M-x harmless-team-compact-plan lists Harmless seats at or
above a percent of the context window. It does not write.
M-x harmless-team-compact records why that seat is compacted
and tells the room. The seat must already be at that wall.
M-x harmless-team-compacts reads those notices, oldest
first. The transcript summary is M-x harmless-compact.
Nothing is started.
The model calls the same commands through tools.
team_boot boots a spec. team_inspect reads the seats, the
edges, the launch order, and the rows, and it writes nothing.
team_message sends text to an occupied seat. team_snapshot
stores a named copy. team_restore puts that copy back when
every seat can return. A detached chat is not offered these
tools. Plan mode refuses the ones that write. Nothing is
started.
A machine is a name and a directory. The directory is that
host's Harmless data. M-x harmless-machine-add records both.
It does not open a connection or store a secret.
M-x harmless-machine-list returns the names.
M-x harmless-machine-show returns one.
M-x harmless-machine-remove forgets the name and leaves the
directory in place. The registry is machines.json under the
local Harmless data directory.
M-x harmless-machine-call runs one public team command with
that host's directory as the data directory. The same verbs
are the harmless-team- commands. The registry stays local.
M-x harmless-machine-copy copies one relative file from one
host to another. A directory, a path that leaves the host, and
a file that is already there are refused. Nothing is started.
A person is an address, name@slack. M-x harmless-person-add
records the name and a Slack channel. A leading # is not
stored. M-x harmless-person-list returns the names. A
missing file is an empty list and writes nothing.
M-x harmless-person-show returns one person.
M-x harmless-person-remove forgets the name. Rows already
opened for that address stay.
M-x harmless-person-ask opens a queue row to that address.
The body is the question. The row's history is the receipt:
the ask, then whether Slack was posted. The token is
SLACK_BOT_TOKEN in the environment, and only when a sender
is set. It is not written into the rig or the people file.
With no sender, the receipt says Slack was not posted.
Nothing is started.
The people file is RIG.people.json beside the rig. It is
not part of the rig. A path, ~, and the filesystem root
are refused and are not expanded.
A profile is a name and a runtime. M-x harmless-profile-add
records the name, the runtime, and the environment variable
that holds the token. The token is not stored.
M-x harmless-profile-list returns the names. A missing file
is an empty list and writes nothing.
M-x harmless-profile-show returns one profile and whether
that variable is set. It does not return the token.
M-x harmless-profile-remove forgets the name. A seat that
already names it keeps that name and its conversation.
M-x harmless-profile-switch points an occupied seat at a
profile of the same runtime. The session and the inbound
work stay. The rig stores the profile name and never the
token. This is not M-x harmless-login. Nothing is started.
The profile file is profiles.json under the local Harmless
data directory. It is not part of the rig. A path, ~, and
the filesystem root are refused and are not expanded.
M-x harmless-team-health reads what is true. A seat is
empty, live, missing, or disagree. A service is up
or down, and it is reported beside the seats, not as one of
them. The command writes nothing and starts nothing.
M-x harmless-team-service records a name and a process id or
an absolute path. A missing path is not created. A process
is not started. M-x harmless-team-service-remove forgets the
name and leaves the path and the process in place.
M-x harmless-team-recover rewrites a seat when the live
session and the record disagree. A missing session is cleared.
A Harmless summary that names another real session is
retargeted. The live files stay. An empty seat and a live
seat are left unchanged.
M-x harmless-team-fleet shows one rig: its launch order, its
edges, the health of its seats and services, and the rows that
are waiting on a person. Services stay beside the seats. The
session dashboard still lists sessions by project. The fleet
view writes nothing and starts nothing.
A project directory holds missions, and a mission holds slices.
M-x harmless-scope-project writes SPEC.md at the project.
M-x harmless-scope-mission writes missions/NAME/SPEC.md.
M-x harmless-scope-slice writes
missions/NAME/slices/NAME/SPEC.md. Each file starts with
# Intent. Only a slice also has # Specification. Reading
a slice composes the project intent, the mission intent, and
the slice intent. M-x harmless-scope-show reads that and
writes nothing.
The specification is one item per non-blank line.
M-x harmless-scope-proof stores evidence as
proof/NAME.md under the slice. M-x harmless-scope-judge
appends one attributed accept, reject, or withdraw to
judgments.json. The judgment names the judge, the reason,
and the proof. M-x harmless-scope-progress derives
proved, rejected, or open from the latest judgment whose
proof file is still there. Progress is not stored. A name is
letters and digits. / is text, and it is not opened as a
path. Nothing is started.
A project holds context packs under packs/NAME/PACK.md.
M-x harmless-context-list reads the pack names.
M-x harmless-context-show reads the manifest.
M-x harmless-context-get reads a ref: a pack, one file in
that pack, or one heading in that file. A heading matches
its text or its slug.
M-x harmless-context-profile composes the pieces the pack
declares for fresh, handover, or post-compaction. The
piece list comes from the manifest. Four characters are one
token. The total is reported, and a budget reports how far
the text is over. The text is not cut. Nothing is written,
and nothing is started.
spawn_agent starts a child session with its own transcript and waits
for that child's last reply. prompt is the task. An optional
agent name loads NAME.md from .claude/agents/, .codex/agents/,
.harmless/agents/, .grok/agents/, or .agents/agents/, from the
session directory up through your home directory. A closer file wins.
The file's frontmatter can set tools, model, and sandbox
(workspace or read-only). The body is extra instructions for the
child. A subagent cannot start another subagent. Aborting the parent
aborts a child that is still running.
run_workflow runs one script, starts the child sessions that script
asks for, and returns one string. Pass name to run a fixed
NAME.hw file, or script to run text written for this task. Fixed
scripts live in .claude/workflows/, .codex/workflows/,
.harmless/workflows/, .grok/workflows/, or .agents/workflows/,
from the session directory up through your home directory. A closer
file wins. The script is a list of let and complete forms. It is
not Emacs Lisp. A subagent cannot run a workflow. Aborting the
parent aborts a child the workflow started, and the result is not
added. An agent form can pass :worktree NAME to edit an isolated
checkout.
enter_worktree points the session at a git checkout of the same
repository. The checkout is stored under harmless-directory, outside
the project, on a branch named harmless/ plus the name. It starts at
the current HEAD. Uncommitted files are not copied. The same name
is the same checkout. exit_worktree returns to the previous project
and leaves the checkout on disk. M-x harmless-worktree and M-x harmless-worktree-exit do the same from the minibuffer. spawn_agent
takes worktree so the child edits that checkout while the parent
stays put.
list_sessions lists the sessions that are live in this Emacs. A
session that is only on disk is omitted. send_message delivers
text to one of those ids. An idle session gets the finding in its
transcript immediately. A session that is in a turn receives it at
the next safe point. The finding does not start a turn. Closing
Emacs before a queued finding is written drops it.
web_search sends a query to DuckDuckGo over https and returns a
title, a URL, and a snippet for each hit. The class is network, so
accepting edits still asks before a search. The result is not written
into the project.
web_fetch reads one http or https page and returns its text. HTML
is reduced to text. The class is network, so accepting edits still
asks. The page is not written into the project.
x_keyword_search finds recent posts. x_thread_fetch reads one
post and its conversation. x_user_search finds accounts. The
class is network, so accepting edits still asks. The bearer
token is harmless-x-bearer, X_BEARER_TOKEN, or an auth-source
entry for api.x.com. A missing token is refused before a
request. Nothing is written into the project.
mcp.json names local MCP servers. The project file is
.harmless/mcp.json. The global file is mcp.json under the
Harmless data directory. A server tool is offered as
mcp__SERVER__TOOL. Its schema is loaded when the tool is about
to be called. mcp_list_resources and mcp_read_resource read
resources. The class is network, so accepting edits still asks.
A read-only subagent may read resources. Nothing is written into
the project.
computer_screenshot returns the size and a PNG path under the
Harmless data directory. computer_click, computer_type, and
computer_key act on the desktop. browser_open opens an http
or https URL in the desktop browser. The class is shell, so
accepting edits still asks. Plan mode refuses a click, typed
text, a key, and the browser. A screenshot still runs. A
read-only subagent cannot use these tools. Nothing is written
into the project.
monitor watches a file in the project or an added directory, a
command, or a background shell. A line that matches pattern wakes the
session. A file watch does not ask. A command that can write
still asks, and plan mode refuses it. monitor_stop stops the
watch. Nothing is written into the project.
schedule sends a prompt once, after a delay or at a UTC time.
The prompt and the time are saved with the session. Loading the
session arms a schedule that is still in the future and drops one
that has expired without sending it. schedule_cancel removes a
waiting prompt. The class is edit, so accepting edits does not
ask. Plan mode does not refuse it. A read-only subagent cannot
use it. Nothing is written into the project.
todo_write keeps a list of work on the session. Each item has
an id, content, a status (pending, in_progress, completed,
or cancelled), and the ids it depends on. merge updates by
id. False replaces the list. The list is saved with the
session. A fork copies it. A subagent starts empty. The class
is read, so it does not ask. Plan mode does not refuse it.
Nothing is written into the project.
review reads the working tree, a base branch, or one commit.
M-x harmless-review does the same. The menu key is R.
Omit base and commit for the working tree. findings
records the result. Omit findings and the tool returns the
diff. The list is saved with the session. A fork copies it.
A subagent starts without one. The class is read, so it does
not ask. Plan mode does not refuse it. A finding is not
applied as an edit. Nothing is written into the project.
lsp asks a language server that is already running in Emacs.
The operation is definition, references, or diagnostics.
M-x harmless-lsp does the same. The menu key is L.
Line and column start at 1. Diagnostics read the whole file.
Harmless does not start the server. The class is read, so it
does not ask. Plan mode does not refuse it. Nothing is written
into the project.
read_file reads an image by its bytes, a PDF by the literal
strings in the file, and a notebook by its cells. offset and
limit apply only to text. notebook_edit replaces one cell
in an existing .ipynb file. The file is not created. The
class is edit, so it asks in ask mode. Plan mode refuses it.
M-x harmless-attach-image queues an image on the next prompt.
The menu key is I. The queue is not saved. A read does not
change the file.
Memory is Markdown under harmless-directory, in memory/. Global
notes apply to every project. Workspace notes belong to one project
directory. A new fact goes to observations/_inbox/. M-x harmless-dream folds the inbox into topics/ and moves those files
to archive/. MEMORY.md is a generated index.
Each turn sends the index, not the notes. The model reads a note with
memory_read, saves a fact with memory_remember, and replaces a
topic with memory_write_topic. M-x harmless-remember saves a note
directly. A prefix argument stores it in global memory.
A finished turn can also write one inbox observation. Set
harmless-memory-observe-function to a function of the session. It
returns a title and a body. Nil writes nothing. An unset function
writes nothing. The transcript is not modified. A detached chat
records a global observation. An attached project records a
workspace observation.
harmless-permission-rules and harmless-permission-denies are
copied onto a session when it is created and when it is loaded.
A rule is Bash(PATTERN), Edit(PATTERN), or
WebFetch(domain:HOST). An allow skips the question. A deny
refuses before the question, including in accept-edits and
always-approve, and a deny wins. Plan mode still refuses a shell
command that can write. / and c:/ are not edit paths. A rule
set only on the session is not saved. .harmless/permissions is
a per-project allow list that survives the session. One rule per
line. A missing file creates nothing.
harmless-dangerous-commands refuses those programs before the
question. An allow rule does not override it. rm of /, a
drive root, ~, or /* is refused too. auto mode asks a
classifier, or the reviewer model when the classifier is nil,
before a call that would otherwise prompt. allow skips the
question. Anything else asks. An allow rule skips the reviewer.
A deny, a dangerous command, and plan mode still refuse first.
harmless-hooks can block a tool call before the question, or
replace the result the model sees after the tool runs. A hook
does not override plan mode, a deny rule, or the
dangerous-command list, and it does not skip the question. A
hook is a function, a command, an HTTP request, an MCP call, a
prompt, or a subagent. The list is not saved on the session.
M-x harmless-add-directory lets the session read and edit a
directory outside the project. Nil, "", a relative path, /,
and c:/ are refused and are not expanded. The list is saved
with the session. A path outside the project and outside every
added directory is still refused. The shell's remembered
directory stays inside the project.
M-x harmless-plan turns on plan mode for the current session. A
prefix argument turns it off. The same command is P in the Harmless
menu. The model may also ask to enter plan mode.
In plan mode, write_file, replace, and apply_patch fail. A shell
command that can write is refused before any permission question. A
read-only shell command still runs. The model writes plan.md in the
session directory with write_plan, then calls exit_plan_mode. That
shows the plan and asks you to approve it, comment on a line or
range, send revision notes, or quit. A comment does not change
plan.md. Approving turns plan mode off so the same turn can start
changing project files.
The session header shows plan while the mode is on. The flag is
saved with the session.
M-x harmless-usage shows the current session and one row per
connection. The session block is the model's context window, the last
request's prompt size, and what remains in the window, then the running
prompt and completion totals for this chat. Window sizes live in
harmless-usage-context-windows. The account block is the last
rate-limit remainder each provider sent. A connection with no such
headers says so. This is not an account credit balance.
make test # byte-compile with warnings as errors, then ERT
make compile
make clean3-clause BSD. See LICENSE. Copyright (c) 2026 Christopher Mark Gore, Soli Deo Gloria.
