aider¶
Aider (catalog agent aider) is a terminal pair-programming CLI that works inside a git
repository (or the current directory) and keeps its chat and prompt history there as plain-text
files, with a few optional files under the home, so most of its evidence is project-level. It
shares no storage or format with another agent. Paths cited are relative to the clone. The agent
is parsed: the aider parser reads the chat history as the primary store and the input and LLM
histories as secondary sources (Parser plan).
Source and evidence¶
Aider-AI/aider, open source, commit
5dc9490bb35f9729ef2c95d00a19ccd30c26339c,
Apache-2.0
(LICENSE.txt).
All claims from source; the config-key example is from the shipped docs in the same tree. The input
history format comes from the upstream prompt_toolkit history.py, which is not vendored.
Observed¶
- No real install.
Source reviews¶
- 2026-10-21: Aider-AI/aider at
5dc9490:
the parser now yields
model:rows from the announcement lines andpermission decision:rows from the confirmation lines (Model rows, Permission rows); added the exact form of a confirmation answer, the/settingsoutput that is not an announcement, and the source-host statement (Source host rows). - 2026-10-19: the head of Aider-AI/aider
mainis still5dc9490, the commit cited throughout, so there is no drift. Added the confirmation lines and the startup model announcements of the chat history (Record schema), theyes-alwaysstanding setting, the/pastetemporary directory and what/clearand/resetleave (What survives a delete).
Collection¶
Per-user storage¶
Aider has no platform branching; everything is Path.home() (%USERPROFILE% on Windows), so the
layout is identical on Linux, macOS and Windows. No XDG use.
Home-level files, all optional:
~/.aider.conf.yml: the config search is cwd, git root, home (aider/main.py:464-477;aider/args.py:790-796), or--config. At any level it can setyes-always: true, a standing yes to every confirmation (Confirmations).~/.aider.model.settings.ymland~/.aider.model.metadata.json:generate_search_path_listtries home, git root, then cwd or the--model-settings-file/--model-metadata-filevalue (main.py:305-310,336-338,397-399; defaultsargs.py:121-131).~/.env:load_dotenv_filesloads~/.env,<git root>/.env, then.envin cwd or--env-file(main.py:361-385;args.py:31-32,801-804). This is a shared dotfile, not aider-owned.~/.aider/oauth-keys.env: loaded first (main.py:370-374), written by the OpenRouter OAuth flow (aider/onboarding.py:363-367).~/.aider/analytics.json: anonymousuuidand disable flag (aider/analytics.py:139-152).--analytics-log <file>writes JSONL events wherever the user says (args.py:575-578).~/.aider/caches/:model_prices_and_context_window.json(aider/models.py:169-170),openrouter_models.json(aider/openrouter.py:34-35),versioncheck(aider/versioncheck.py:12),help.<version>/llama-index embedding store (aider/help.py:93).- History files default to the git root (Project-local files); when aider runs
outside a repository they land in the cwd
(
args.py:271-276), so~/.aider.chat.history.mdand friends exist only for sessions started in the home directory.
No relocation environment variable; everything is --option or config-file driven. pip/uv
install directories are out of scope.
Scratch directories¶
None known: Aider works in the git root or the cwd and makes no working directory of its own; the
only directory it creates is a system temporary directory for an image pasted with /paste
(aider/commands.py:1295-1298),
outside every catalog line. /clear and /reset delete no file; what remains is in
What survives a delete.
Credentials¶
~/.aider/oauth-keys.env:OPENROUTER_API_KEY="..."appended in plaintext (onboarding.py:365-367)..aider.conf.ymlat any of the three levels acceptsopenai-api-key,anthropic-api-key,api-key: provider=keyandset-env(args.py:69-110; documented exampleaider/website/docs/config/aider_conf.md:81-84,109)..envfiles at home, git root and cwd carry*_API_KEYvalues by design..aider.model.settings.ymlhas no key field;extra_paramsis passed to litellm and could carry one, so flagging it is harmless but not evidence-based..aider.llm.historycontains full request bodies; keys are not logged.- No keychain use.
Exclusions¶
.aider/caches (model price tables, embedding store, version stamp). In repositories:
.aider.tags.cache.v<N> tree-sitter tag cache, currently v3
(aider/repomap.py:35,43,186,218).
Project-local files¶
Defaults placed in the git root, or cwd without git
(args.py:271-276,422-426):
.aider.input.history (prompt_toolkit FileHistory,
aider/io.py:356),
.aider.chat.history.md (appended per session,
io.py:336),
.aider.llm.history only when --llm-history-file is given
(args.py:296-300;
written at
io.py:755-764),
.aiderignore, .aider.tags.cache.v3/, and per-level .aider.conf.yml,
.aider.model.settings.yml, .aider.model.metadata.json, .env. CONVENTIONS.md is a documented
convention loaded with --read
(aider/website/docs/usage/conventions.md),
not a hard-coded name. Aider adds .aider* and .env to the repository .gitignore on first run
(main.py:160-168),
so git-based enumeration misses all of this; the collector must walk the directory.
Where the project path is recorded¶
Nowhere under home. Aider keeps no index of repositories; analytics.json holds only a UUID, and
the chat history lives inside each repository. Discovery sources for other agents (and shell
history) are the only way to find aider projects; the # aider chat started at <timestamp> headers
in .aider.chat.history.md date the sessions once found.
Parsing¶
Transcript stores¶
In the git root, else the cwd
(aider/args.py:271-275):
.aider.chat.history.md (markdown), .aider.input.history (prompt_toolkit FileHistory text),
.aider.llm.history (text; default None, written only when --llm-history-file is set,
args.py:295-299).
One file holds every session in that repository, appended.
What survives a delete¶
Aider has no delete command: /clear and /reset empty the in-memory chat, leave every file and
write no marker
(commands.py:439-443).
The three files are project-local, in the git root, so they go only with the files themselves or the
repository
(args.py:270-275).
.aider.input.history is the per-prompt history file: one entry per prompt with its local time and
no session, which survives the chat history being deleted, truncated or redirected but not the
project (Past use and secondary sources).
.aider.llm.history, written only when --llm-history-file is set, is a second copy of every
prompt sent to the model and every response, each with its own time. No other store of past use was
found. The analyzer reads .aider.chat.history.md as the primary store and the input and LLM
histories as secondary sources on every run (see
Secondary sources).
Record schema¶
.aider.chat.history.md¶
.aider.chat.history.md (append-only,
aider/io.py:1117-1136):
- Session header:
\n# aider chat started at YYYY-MM-DD HH:MM:SS\n\n(local time,:335-336). The only dated line in the file. - User prompt:
\n#### <line1> \n#### <line2> \n; empty input is####followed by a blank (:775-789). Slash commands appear the same way (#### /add foo.py). - Tool and system output, including
Applied edit to <path>,Commit <hash> <msg>, errors and confirmations:> text \nblockquote lines (:966-973,:995-999,base_coder.py:2334,repo.py:313). - Startup announcements: at the start of each run, and again after a
/model,/weak-model,/editor-modelor/chat-modeswitch, Aider writes its announcement lines as blockquotes (base_coder.py:550-554,main.py:1042,1174-1180):> Aider v<version>, then> Model: <name> with <format> edit format(or> Main model: ...when a separate weak model is in use), with, <n> think tokens,, reasoning <effort>,, prompt cacheor, infinite outputappended when they apply,> Editor model: <name> with <format> edit formatin architect mode,> Weak model: <name>, and> Git repo: <dir> with <n> filesor> Git repo: none(base_coder.py:207-260)./settingsprints the same announcement lines inside one multi-line message, andappend_chat_historyputs>before the first line of a message only, so only its> Aider v<version>line is a blockquote and the model lines that follow it read as assistant text (commands.py:1432-1460,io.py:1117-1121)./model <name>itself is a#### /model <name>user line (commands.py:87-112). - Confirmations:
confirm_askwrites one blockquote line> <question> (Y)es/(N)o[/(A)ll][/(S)kip all][/(D)on't ask again] [<default>]: <answer>, the answer as typed (y,n,a,s,d, or empty for the default), preceded by the subject (the command, path or URL) on blockquote lines of its own (io.py:807-858,903-905,922-923). The line isf"{question.strip()} {res}"withresreduced to the lower-case first letter of the answer, the default filled in for an empty one, so the bracketed default ends in]:and one of the five letters follows (io.py:900,904,922). An answer taken from an earlier "All" or "Skip all" of the same group (several URLs or files asked about together) is also written as a#### <question><answer>user line before the blockquote (io.py:870-872). The questions includeRun shell command?,Create new file?,Add file to the chat?andAdd URL to the chat?(base_coder.py:977,1773,2207,2455). Under--yes-always(yes-always: truein a config file,args.py:760-764) the answeryis written without asking, andnfor a question that requires an explicit yes (io.py:866-867), so a line alone does not say whether a person answered.Run shell command?is such a question (base_coder.py:2456-2461), so under--yes-alwaysa suggested shell command is declined with annline. "Don't ask again" adds the question to an in-memory set for the run, and a later asking of the same question is declined without a line (io.py:269,821-824,903). - Assistant: raw markdown with no prefix,
\n<content>\n\n(:793-795, called frombase_coder.py:1829), so SEARCH/REPLACE edit blocks appear verbatim inside fenced code. The function-call fallback writesjson.dumps(args)(:1834). - Aider's own reader
(
utils.py:148-188): skip lines starting#;>is tool output;####is user; anything else is assistant.
.aider.input.history¶
.aider.input.history (prompt_toolkit FileHistory,
io.py:19,356,740;
format prompt_toolkit/history.py:298-307): an entry is \n# YYYY-MM-DD HH:MM:SS.ffffff\n followed
by one +<line> per input line (local time).
.aider.llm.history¶
.aider.llm.history
(io.py:754-764):
TO LLM YYYY-MM-DDTHH:MM:SS\n followed by format_messages output
(utils.py:112-134):
a ------- separator, then ROLE <line> per content line (SYSTEM, USER, ASSISTANT), and
LLM RESPONSE <ts>\n plus ASSISTANT <line> lines
(base_coder.py:1793,
:1822-1826).
Joins and timestamps¶
The project is the directory containing the files (the git root). There is
no session id; synthesise one per # aider chat started at header. Input
history timestamps join to #### prompts by order and text. No branch is
recorded; the model is named only in the startup announcement lines
(Record schema), which carry no time of their own and
follow the session header.
Other stores¶
None besides the input and LLM histories of Transcript stores, and no SQLite
store. .aider.tags.cache.v* is a repository-map cache, not chat
(Exclusions).
Format versions¶
None; the format has not changed across sessions.
Secrets¶
.aider.llm.history contains the complete system prompt with every added
file; .aider.chat.history.md contains any pasted secrets. Aider writes no
keys itself. The credential files are in Credentials.
Parser plan¶
Globs: **/.aider.chat.history.md, **/.aider.input.history,
**/.aider.llm.history (project-level, so they arrive through the
collector's project catalog).
Rows: one per #### block (user), per assistant run, per > run
(tool_result); # aider chat started becomes system.
| Column | Source |
|---|---|
| timestamp_utc | last # aider chat started at header, or the matching # input-history line, converted from local time |
| session_id | synthesised <path>#<header timestamp> |
| project_path | the directory holding the file |
| git_branch | empty |
| model | the new model on a model: row; empty on every other row |
| tool_name, tool_use_id | empty |
| text | prompt text, assistant text, blockquote text |
Permission rows¶
Each confirmation line (Record schema) is a system row after the
tool_result row of its blockquote run, at that run's time, with source_line the confirmation
line: permission decision: <decision> (<question>) answer=<letter> default=<default>, where
<decision> is yes, no, all, skip all or no, don't ask again for the letters y, n,
a, s and d. A y or n answer adds ; may be automatic (--yes-always), since
--yes-always writes the same line without asking. The subject stays in the tool_result row,
the blockquote text before the question. A question declined without a line after "Don't ask
again" yields nothing. Source-derived; not observed on any real install.
The yes-always key of a collected .aider.conf.yml is a standing setting, but the parser does
not read the file: it is YAML, which the analyzer has no reader for, so no permission setting:
row is written for it.
Model rows¶
A blockquote run that holds a Model: or Main model: announcement line
(Record schema) is read for the model lines, so the /settings output and a
stray Weak model: line elsewhere are not. After the run's tool_result row the parser writes a
system row per change, with source_line the announcement line and the model column the new
model: model: <name> at a session's first announcement and model: <old> -> <new> on each later
one that names another main model; (editor model) or (weak model) follows the same text for
the Editor model: and Weak model: lines. A Model: line means the weak model is the main
model, so it sets the weak model without a row unless it replaces a separate one. The model state
starts afresh at each session header. The edit format and the options after it (think tokens,
reasoning effort, prompt cache) are not in the row. Source-derived; not observed on any real
install.
Past-use rows and secondary sources¶
Sources: .aider.chat.history.md is primary; .aider.input.history and
.aider.llm.history are secondary, parsed on every run, with each row's
own time and the default two-second window.
.aider.chat.history.md is the store of record: it is the only file with session headers and
assistant and tool output, and the one Aider itself reads back with --restore-chat-history
(base_coder.py:519-520).
The input and LLM histories are secondary sources (files that repeat some of a primary store's
events, in the sense of
doubleagent issue 122):
- Every prompt typed at the terminal is written to both: prompt_toolkit's
FileHistoryon the prompt session appends it to the input history (io.py:355-356), andget_inputthen callsuser_input, which writes the####lines (io.py:732-733,:775-789). A--messageprompt is added withadd_to_input_history(main.py:1126-1127,io.py:736-745) and written to the chat history by the coder (base_coder.py:877-879). The parser stamps the chat row with the matching input-history entry's time, so the two rows carry the same text, synthesised session id, project path and timestamp, and pair exactly. - Input-history entries with no chat twin: a
/run <command>entry Aider adds when it runs a shell command it suggested, whose chat trace is a> Running <command>blockquote instead (base_coder.py:2471-2474); every prompt after a chat-history write error, which setschat_history_filetoNonefor the rest of the run (io.py:1133-1136); and every prompt of a run whose--chat-history-filepointed elsewhere, since the two paths are separate options (args.py:276-287). These rows are kept. - What the input history survives is in What survives a delete.
- The LLM history's
TO LLMblocks becomesystemrows that no chat row repeats. ItsLLM RESPONSEblock is timestamped when the response arrived (io.py:754-764), while the chat history's assistant row inherits the prompt's time and may be split by>lines, so anassistantrow pairs only for a reply of identical text that arrived within the two-second window. Expect the LLM history's rows to stay in the timeline.
Sync, relay and origin rows¶
Aider records no sync state, because it uploads no session: no command or setting sends a chat
history, input history, LLM history or conversation title anywhere beyond the prompt sent to the
model provider through litellm (the library Aider calls for every model API). The commands are all
local
(commands.py);
/save writes /add and /read-only lines to a local file
(commands.py:1497-1522),
/copy uses the clipboard, and /clear empties the in-memory message lists without touching the
history files
(commands.py:435-437).
What does leave the host:
- Analytics, through PostHog (a hosted product-analytics service) at
https://us.i.posthog.com; the client for Mixpanel, another such service, is commented out (analytics.py:55-57,analytics.py:101-108). Opt-in: a random 10% of users are asked once,--analyticsasks anyone, and analytics stay off unless the user said yes (analytics.py:119-135,main.py:642-662). Events carry event names, a model name (redacted to<provider>/REDACTEDwhen it is not a known model), token counts, costs, edit format and system information (analytics.py:186-240,base_coder.py:2113-2122), never prompt or reply text; the exceptions are themessage_send_exceptionevent'sstr(err)(base_coder.py:1511) and PostHog's own exception autocapture (analytics.py:106), which can carry whatever a provider error message quoted. The opt-in state is~/.aider/analytics.json, keysuuid(string),permanently_disable(boolean ornull) andasked_opt_in(boolean) (analytics.py:137-184), collected by the.aidercatalog line; it says analytics may have been on, not what any session sent.--analytics-log <file>appends each event as a JSON line withevent,properties,user_idandtime(epoch seconds) to a path the user chooses (args.py:574-578,analytics.py:242-253); no catalog line can name that path, but ananalytics-log:key in a collected.aider.conf.ymldoes.--analytics-posthog-hostand--analytics-posthog-project-api-keyredirect the events to another PostHog (args.py:585-594). - Crash and
/reporttext, which goes nowhere by itself: Aider builds a prefilled GitHub new-issue URL (version, platform, the traceback with file paths reduced to base names, or the startup announcements) and opens it in the browser for the user to submit (report.py:37-90,report.py:94-151,commands.py:1555-1567).
The OpenRouter OAuth flow writes ~/.aider/oauth-keys.env (see Credentials) and
carries no session. Searched for sync, share, upload, publish, cloud, remote, gist,
paste, export, backup, account, team, session_id, conversation_id, remote_id and
outbound requests/httpx calls in aider/ (the website tree aside); the only hosts contacted are
the model providers, PostHog, OpenRouter's auth and model-list endpoints, GitHub's Copilot token
endpoint when a github_copilot/ model is used with GITHUB_COPILOT_TOKEN
(models.py:950-983),
litellm's price table and pages fetched by /web. No other agent's directory is read or written for
this. The parser yields no session synced: row.
No session is relayed or of remote origin.
Scratch directory rows¶
None (Scratch directories).
Deletion rows¶
No marker; the files outlive /clear and /reset
(What survives a delete).
Bad-line rows¶
A non-blank input-history or LLM-history line that fits neither format is its own system row
carrying the line in full (error_rows); the chat history's markdown has no line that fails to
parse, since every line is a header, a prompt, a blockquote or assistant text.
Fixtures¶
Fixture .aider.chat.history.md:
# aider chat started at 2026-10-02 12:00:00
#### rename foo to bar
Here is the change:
a.py
<<<<<<< SEARCH
foo
=======
bar
>>>>>>> REPLACE
> Applied edit to a.py
> Commit abc1234 refactor: rename foo to bar
Announcement and confirmation lines, after a session header and after a /model switch:
> Aider v0.86.1
> Main model: anthropic/claude-sonnet-4-5 with diff edit format, prompt cache
> Weak model: anthropic/claude-haiku-4-5
> Git repo: .git with 3 files
> pytest -q
> Run shell command? (Y)es/(N)o/(D)on't ask again [Yes]: n
> Main model: claude-opus-4-5 with architect edit format
> Editor model: claude-sonnet-4-5 with editor-diff edit format
> Add URL to the chat? (Y)es/(N)o/(A)ll/(S)kip all/(D)on't ask again [Yes]: d
The other two files:
.aider.input.history:\n# 2026-10-02 12:00:05.123456\n+rename foo to bar\n.aider.llm.history:TO LLM 2026-10-02T12:00:05\n-------\nSYSTEM Act as an expert\n-------\nUSER rename foo to bar\nLLM RESPONSE 2026-10-02T12:00:09\nASSISTANT Here is the change:\n
Source host rows¶
The agent records no source-host integration. A grep of aider/ (the website tree aside) at
5dc9490 for
github, gitlab, bitbucket, oauth, installation, repositor, pull request and clone
found no account sign-in, repository list or remote conversation: the one OAuth flow is
OpenRouter's (Credentials), the GitHub hit is the Copilot model provider, whose
GITHUB_COPILOT_TOKEN is read from the environment and exchanged in memory for an API key that is
never written
(models.py:950-983),
and git use is the user's own repository.
Confidence¶
- High: all file names and search orders (source); chat and LLM history formats (read from the writers).
- Medium-high: input history (upstream prompt_toolkit source, not pinned to Aider's dependency version). Local-time timestamps need the host time zone to convert exactly.
- Not determined: nothing material.