a-agent
a is a fast, single-process terminal coding agent written in Rust, for people
who already live in a shell.
It stays at your prompt, leaves your scrollback intact, and already carries the
context your shell just produced. In Fish, Ctrl+G turns the current prompt
into an a> prompt:
❯ cargo test
test drops_punctuation ... FAILED
…
error: test failed, to rerun pass `--test slug`
a> fix this once · tab
▸ Reasoning
✓ read /tmp/demo/src/lib.rs · 3 lines
│ 1: pub fn slug(title: &str) -> String {
│ 2: title.to_lowercase().replace(' ', "-")
│ 3: }
│ The test expects punctuation to be dropped. The current `slug` only replaces
spaces with hyphens, so `"Hello, World"` becomes `"hello,-world"` instead of
`"hello-world"`.
✓ apply_patch 1 files +6 -5
M /tmp/demo/src/lib.rs
✓ bash exit 0
$ cd /tmp/demo && cargo test
│ running 1 test
… output truncated
│ Fixed. The test now passes.
❯
Excerpt of a real session; some tool calls are omitted. a inherited the
failing command, its exit status, and the directory from the shell — nothing
was pasted in.
It is also a plain CLI, usable from any shell:
|
Install
Rust 1.95 or newer is required. The installed binary is named a. Then install
the shell integration:
To build from a checkout instead:
The agent runs in any terminal and any shell. The Ctrl+G prompt, the
command-history context, and the per-shell conversations target Fish.
Why a-agent
- Shell-native.
Ctrl+Genters and leaves AI input on the same Fish prompt line, and each Fish process gets its own conversation per directory. - It knows what you just ran. Fish hooks record command, cwd, exit status, and duration — never output — and the runtime injects the recent ones.
- Append-only output. No viewport, alternate screen, or repainted history; your scrollback stays scrollable and copyable.
- No indexing, no daemon. Startup does not scale with repository size; resuming a 100-turn conversation reaches the first request in about 15 ms.
- Three tools.
read,apply_patch,bash. Independent calls run in parallel; patches touching the same file are serialized. Abashcall can raise its own timeout for a release build or a full test run. - Resumable and reversible. SQLite sessions,
Escto rewind to an earlier message, compaction anchored on the token usage the provider reported. - Any provider. Anthropic Messages, OpenAI Responses, and OpenAI-compatible Chat Completions, with third-party base URLs and headers as configuration.
Configure
The first agent run creates ~/.config/a/config.toml from
config.example.toml and prints its path. Configuration
uses named provider and model profiles; one provider can serve several
independently configured models.
= "codex"
[]
= "responses"
= "https://api.openai.com/v1"
= "OPENAI_API_KEY"
[]
= "openai"
= "gpt-5.6"
= "medium"
= ["low", "medium", "high", "xhigh", "max"]
= 1050000
[]
= "openai"
= "gpt-5.6"
= "low"
= ["none", "low", "medium"]
type is responses, anthropic, or chatcompletion. A provider may set
api_key directly, which takes precedence over api_key_env at the cost of
storing the secret in plaintext. Anthropic's SDK appends /v1, so configure an
origin for it; OpenAI-protocol base URLs normally include /v1.
Third-party gateways can extend requests:
[]
= "chatcompletion"
= "https://gateway.example/v1"
= "GATEWAY_API_KEY"
[]
= "acme"
[]
= "priority"
Provider profiles own endpoints and authentication; model profiles own the model
ID, effort choices, context_window, max_tokens, and optional header or
request overrides. context_window must exceed max_tokens. Nothing is probed
at startup, and only the selected provider is initialized. Terminal display
limits live under [ui]. Project-local .a/config.toml overrides the global
file.
Use
-1 exits after one complete user turn, including all model and tool cycles.
Target files are sent as paths and read only if the model calls read.
Interactive a defaults to multi-turn mode; Tab switches between
multi · tab and once · tab.
Piped stdin is bounded and keeps the tail, so cargo test 2>&1 | a "fix this"
sends the useful end of a long log. Only a pipe or a redirected file counts as
input: a supervisor that hands its child a socket is not piping anything, and
reading that would block forever. A real pipe is read to end of file however
long the producer takes. Like any program that reads stdin, a inside a
while read loop would consume the rest of that loop's input; redirect it there
with a ... < /dev/null.
Interactive commands:
/model [profile]
/effort [level]
/thinking
/status
/clear
/compact
/resume [session-id]
/help
Typing / lists them. /model, /effort, and /resume open a selector when
given no argument. Model, effort, and reasoning visibility persist with the
session and are restored on resume. /resume only offers sessions from the
current directory.
Typing @ anywhere in the prompt completes a path from the current directory:
Up/Down cycle the matches, Tab accepts one, and accepting a directory
lists it so the next Tab descends. Only one directory is read per keystroke,
and .git, node_modules, and target are skipped. A mentioned path is sent
as a target for that turn, exactly like a path given on the command line, so the
file is still only read if the model calls read.
When a model profile sets context_window, a compacts automatically near
context_window - max_tokens. Usage comes from the token counts the provider
reports, so no extra API call is made; only messages newer than the last report
are estimated locally.
/status also reports what the session has cost. Prices come from
models.dev, fetched only when /status runs and then
cached for a day, so startup never pays for it. A model id that several
providers price differently is not guessed at: /status names the candidates
and the line to add, because a plausible but wrong number is worse than none.
[]
= "deepseek/deepseek-v4-flash" # a models.dev provider/model key
[] # or state the prices yourself,
= 0.14 # in USD per million tokens
= 0.28
= 0.0028
Explicit cost wins and needs no network. A_PRICING_URL points the lookup at
a mirror. The total covers every request the session made, including ones on
branches a rewind left behind.
Context
Active AGENTS.md files are loaded along the current/target path ancestry, from
broad scope to specific scope. The optional global file is:
~/.config/a/AGENTS.md
Skills use the Agent Skills format: a directory whose
SKILL.md carries name and description in YAML frontmatter, plus any
bundled scripts/, references/, or assets/. Only the shared convention is
scanned, so skills installed by any compliant client are visible here and vice
versa:
~/.agents/skills/<name>/SKILL.md
<project>/.agents/skills/<name>/SKILL.md
Direct child directories of those two locations are scanned. A project skill takes precedence over a user skill of the same name.
Only name, description, and the path are loaded at startup; the model
reads the body when a task matches. A skill missing a description is skipped
with a warning, and a name that disagrees with its directory is warned about but
still loaded.
Sessions
Sessions live in $XDG_STATE_HOME/a/sessions.db, or
~/.local/state/a/sessions.db when XDG_STATE_HOME is unset. Resuming replays
the history it is about to continue. Cancelling a turn records an explicit
notice, so a later resume does not silently continue the cancelled task.
Esc: rewind to an earlier message of yours; the old branch is keptEscorCtrl+Cduring a turn: cancel itCtrl+O: toggle reasoning visibility, configurable asui.reasoning_toggle = "ctrl-r"Ctrl+Cat the prompt: discard the typed line, or exit when it is already empty.Ctrl+Yputs a discarded line back
Every apply_patch prints its own hunks, so what was written is visible without
opening the file. Long patches are cut at ui.patch_diff_max_lines (24).
bash runs under tools.bash_timeout_seconds (120) unless the call asks for
longer, which it should for release builds, dependency installs, and full test
runs. Requests are capped at tools.bash_max_timeout_seconds (1800), and a
raised limit is shown next to the command so a long-running step does not look
like a hang. A timeout reports both the limit that applied and the ceiling, so
the retry can ask for enough time.
Rewinding can put the files back too. It first lists what it would do to each
file — delete for one created after that point, restore with line counts for
one that was edited, recreate for one that was deleted — and then offers three
choices: rewind the conversation only, rewind and revert the files, or cancel.
A file the agent touched several times counts once, reverting to its state at the
rewind point. A file that changed since the agent last wrote it is marked keep
and left alone, so a rewind never discards an edit of yours; so is one whose
previous contents exceeded session.snapshot_max_bytes (1 MiB), which is
reported rather than half-restored. There is no separate /undo: rewinding to
the most recent message is the same operation.
Fish
a --install-fish writes ~/.config/fish/conf.d/a.fish. Restart Fish, or
source that file in an already-running shell.
Ctrl+G opens the a> prompt. Ctrl+G again returns your text to the normal
Fish editor on the same line; Ctrl+C clears the line and cancels on an empty
one. Tab switches between
once · tab and multi · tab, and the choice lasts for the life of that Fish
process.
Each Fish process has its own conversation per directory, so two panes in one
repository do not collide. a --resume picks up a directory's latest
conversation regardless of which shell created it.
The hooks record command, cwd, exit status, pipe status, and duration — never stdout or stderr — and the runtime injects the recent ones from that shell and directory into the request's system context.
Security
read, apply_patch, and bash run with your permissions and are not
sandboxed; they can reach paths outside the cwd. apply_patch updates existing
files in place, so permissions, ownership, hard links, ACLs, and extended
attributes stay attached to the same inode; new files use your normal umask. API
keys come from the selected provider profile or its environment variable and are
never written to SQLite.