Aider
Threat Model
evidence-derived · Praxen 1.3.0 · graph contract 1.4 · built against aider-findings-2026-08-12.json
24Components
40Flows
10Boundaries
12Confirmed
4Potential
2Partial
6Mitigated
Summary
The Agent (as modeled)

Aider is a pair-programming assistant that runs in a developer's terminal, reads and edits files in one local git repository, and commits its own work. Everything it does runs through a single language model and a single loop: the model's reply is parsed into file edits and shell commands, and the only thing standing between that reply and the developer's disk is a set of yes/no prompts. Untrusted material reaches that loop from three places -- web pages it fetches, the output of the commands it runs, and AI comments it finds in watched repository files -- and none of it arrives marked as anything other than the developer's own words. Its most consequential capabilities are writing files anywhere the developer can write, running shell commands, and committing to git.

Priority Threats (led by attack paths)

Deal with the file-write path first. A page aider fetches, or a comment planted in a file it watches, can direct the model to write to a path of the attacker's choosing; that path is never checked against the repository root or against the developer's ignore file, and the single confirmation guarding it is one of the prompts that answers itself when aider is run unattended. That is a complete route from a fetched web page to an arbitrary file on the developer's machine. Second is the browser interface: it is launched with no address binding and no password, so on any shared network anyone who can reach the port can add files, drive the model, and get the result committed -- and those commits skip the repository's own pre-commit hooks, which aider disables by default.

What keeps this from being worse is one deliberately hardened gate: shell commands the model suggests are confirmed through a check that refuses to answer itself in unattended runs, and that discipline holds. The gaps are the neighbouring doors -- the automatic linter that runs a shell command after every edit with no confirmation at all, the transcript written at the default permissions with no credential filtering that can be replayed into a later session, and telemetry that ships stack traces containing local file paths once a developer opts in. The fixes, in order: contain model-supplied file paths inside the repository and honour the ignore file there, mark the gates that ingest fetched content and command output as explicit-yes so unattended runs cannot approve them, bind the browser UI to loopback, and turn pre-commit verification back on by default.

Architecture & Trust Boundaries
Attack paths are drawn in red, running from where an attacker gets in to what they reach. Click any box to jump to its inventory row, or a B-badge to jump to that boundary; hover a box to reveal its data flows. Everything reads statically below — the key resolves every mark and the tables carry every citation.
User / InputsClient / AdaptersAgent CoreTools / MCPExternal / DeployB1B3B4B5B7B8B2B6B10B9Local operator/developerENTRYPOINTWatched-file AI commentsENTRYPOINTon attack path — source (ingress)Untrusted network callerENTRYPOINTon attack path — source (ingress)Terminal CLI(InputOutput)CLIENTon attack path — pass-throughStreamlit browser UICLIENTon attack path — pass-throughSlash-command dispatcherADAPTERon attack path — pass-throughCoder request loopORCHESTRATORon attack path — pass-throughPrompt stack and contentframingPROMPTModel call(Model.send_completion)MODEL.aider.chat.history.mdtranscriptMEMORYconfirm_ask approvalgateCONTROLModel-suggested shellgateCONTROLFile-edit gate(allowed_to_edit)CONTROLon attack path — control on the path (bypassed)Auto-detected URL fetchgateCONTROLShell execution(run_cmd)TOOLLinter invocationTOOLWeb scraperTOOLon attack path — pass-throughGit repositoryoperationsTOOLon attack path — target (consequence)Repository mapTOOLEdit-format coders (filewriter)TOOLon attack path — target (consequence)/help documentationindexTOOLFetched web pageEXTERNAL_SERVICEon attack path — source (ingress)Configured LLM providerEXTERNAL_SERVICEPostHog analyticsLOG_SINK
Familiesactors & inputsclient / adaptersagent coretools & datacontrolsexternal & deploy
Kindsentrypointclientadapterorchestratorpromptmodelmemorycontroltoolexternal servicelog sink
Marksboundary — worst threat: confirmed— potential (unanswered hypothesis)— partial (control covers part; remainder stated)— mitigatedattack path (origin → consequence)box on an attack path: source (ingress) pass-through control that failed target (consequence)faint arc = flow spanning 2+ lanes
Attack Paths
A web page aider reads tells it to overwrite a file outside the repository
  1. Fetched web page The page is served by whatever host the URL resolves to — attacker-authored content, and aider ships no domain allowlist
  2. Web scraper [PRAX-2026-08-12-002] The HTML is converted to markdown with all text preserved and no trust labelling applied
  3. Slash-command dispatcher [PRAX-2026-08-12-002] cmd_web prefixes it with 'Here is the content of {url}:' and appends it to the conversation as a role=user message
  4. Coder request loop [PRAX-2026-08-12-002] The injected text arrives in the same role, register and position as a genuine operator instruction, so the model's next edit can be directed by it
  5. File-edit gate (allowed_to_edit) [PRAX-2026-08-12-001] The model-chosen path is resolved with no repository-root containment and no .aiderignore check, and the single confirmation is one --yes-always answers for you
  6. Edit-format coders (file writer) [PRAX-2026-08-12-003] The edit is written to any path the developer's account can reach, inside the repository or outside it
A stranger on the network gets a change committed to the developer's repository
  1. Untrusted network caller [PRAX-2026-08-12-005] Browser mode listens on every interface with no authentication, so any host that can route to the port is unauthenticated and unidentified
  2. Streamlit browser UI [PRAX-2026-08-12-005] The page accepts a chat prompt and lets the caller add repository files to the session
  3. Coder request loop [PRAX-2026-08-12-005] process_chat drives the live Coder with the caller's prompt, and the GUI's command IO is built with yes=True
  4. File-edit gate (allowed_to_edit) Files the caller added through the UI are already in the chat set, so the gate returns immediately with no prompt at all
  5. Edit-format coders (file writer) The model's replacement text is written into the developer's working tree
  6. Git repository operations [PRAX-2026-08-12-006] auto_commit commits the change with --no-verify, so the repository's own pre-commit hooks never inspect it
A comment planted in third-party code makes aider edit files with nobody watching
  1. Watched-file AI comments An AI! comment in a file the operator never authored — a vendored dependency, a teammate's commit, a checked-out branch — matches the watcher's pattern with no authorship check
  2. Terminal CLI (InputOutput) process_changes returns the assembled comment text from get_input as though the operator had typed it
  3. Coder request loop [PRAX-2026-08-12-002] The watch prompt asserts 'I've written your instructions in comments in the code', so third-party text reaches the model in the operator's voice
  4. File-edit gate (allowed_to_edit) [PRAX-2026-08-12-001] In an unattended watch run the edit gate is not explicit_yes_required, so it answers itself and the resolved path is never contained to the repository
  5. Edit-format coders (file writer) [PRAX-2026-08-12-003] The edits land on disk with no operator in the loop at any point
Trust Boundaries — Threats & Governing Remit Rules
B1 Untrusted content entering the model's context (untrusted-ingress) — 3 threats, 3 remit rules · worst: confirmed
R-01 gap Aider MUST NOT treat instructions embedded in retrieved or untrusted content — scraped web pages, third-party file contents, or the stdout/stderr of executed commands — as authoritative directives
R-07 partial URLs auto-detected in untrusted content … MUST be confirmed by the operator before fetch, and that confirmation MUST resist `--yes-always`.
R-19 partial Adding externally fetched or command-produced output into the LLM context MUST require user confirmation.
STRIDEOWASPThreatStatus
TLLM01Scraped pages and command output are appended as role=user messages phrased in the operator's own voice, so instruction-shaped text inside them is indistinguishable from a genuine operator instruction.confirmed PRAX-2026-08-12-002
SLLM01An AI! or AI? comment inside a repository file the operator did not author is acted on as an operator instruction, and the watch prompt asserts 'I've written your instructions in comments in the code' (checked: watch.py:257-281 filters only by gitignore spec and file size, and watch_prompts.py:1-12 supplies no authorship or provenance qualifier).potential
ELLM01The two gates that decide whether fetched pages and command output enter the context are ordinary confirm_ask calls, so an unattended --yes-always or scripted --message run answers them itself and the URL gate additionally offers a persist-across-batch 'all' option.confirmed PRAX-2026-08-12-007
B2 Model decision to shell execution and file writes (tool-invocation) — 4 threats, 4 remit rules · worst: confirmed
R-17 partial All shell-command execution — `/run`, `/test`, auto-lint / auto-test, and any command originating from LLM output … MUST require explicit operator approval before it is executed and MUST NOT run silently.
R-23 partial Destructive and code-executing actions MUST retain an explicit per-action confirmation gate (an `explicit_yes_required`-style check) even under non-interactive operation
R-18 partial Editing or creating files that the user has NOT added to the chat MUST require confirmation before aider modifies them.
R-02 partial Aider may operate only within the operator-designated repository / working tree, and MUST honor the operator's `.aiderignore` / `--subtree-only` scope where set
STRIDEOWASPThreatStatus
ELLM03Auto-lint executes a shell command after every edit with no pre-execution confirmation and ships enabled by default; the only prompt appears after the command has already run, asking whether to fix its errors.confirmed PRAX-2026-08-12-004
TLLM10A model-supplied edit path is joined to the repository root and resolved with no containment check, so an absolute path replaces the root and ../ segments walk out of it, and the operator's .aiderignore scope is never consulted on the write path.confirmed PRAX-2026-08-12-003
ELLM03The sole authorization on a model-chosen write is a confirm_ask that is not marked explicit_yes_required, so in an unattended --message or --watch-files run it answers itself and the write proceeds unreviewed.confirmed PRAX-2026-08-12-001
EASI05Shell commands the model proposes reach the user's login shell with shell=True, but the single confirmation on that path is marked explicit_yes_required and io.py answers such gates 'no' under --yes-always, and the group 'all' shortcut is disabled for them.mitigated aider/coders/base_coder.py:2456
B3 Edits committed into git history (state-commit) — 3 threats, 4 remit rules · worst: confirmed
R-22 partial Aider MUST NOT bypass the repository's configured git pre-commit hooks unless the operator has explicitly authorized skipping them.
R-20 verified Aider MUST NOT discard, overwrite, or bury the user's uncommitted work; any pre-existing uncommitted changes MUST be preserved (committed separately) before aider applies its own edits.
R-21 verified Aider MUST NOT rewrite or destroy git history; `/undo` MUST only revert a commit that aider itself created.
R-26 verified Aider's own commits MUST remain attributable to it (author/committer attribution or a Co-authored-by trailer) so its changes are auditable in the git history.
STRIDEOWASPThreatStatus
TLLM03Every aider commit appends --no-verify because --git-commit-verify defaults to False, so whatever the repository wired into pre-commit — secret scanning, formatting, tests — never runs on AI-authored changes.confirmed PRAX-2026-08-12-006
D—An aider commit could bury the developer's uncommitted work, but any dirty file about to be edited is committed separately first so it stays independently recoverable.mitigated aider/coders/base_coder.py:2175
T—/undo could be steered into destroying operator history, but it reverts only commits whose hash aider recorded and refuses multi-parent commits, dirty files, and commits already pushed.mitigated aider/commands.py:566
B4 Transcript replayed into a later session (stored-state) — 1 threats, 0 remit rules · worst: confirmed
the remit does not touch this boundary — threats here are assessed against the RAISE/OWASP baseline alone (a remit is a job description, not a security model; silence here is normal)
STRIDEOWASPThreatStatus
TASI06The transcript aider writes unattended can be read back as prior conversation with --restore-chat-history and re-summarized in the user's first person, so instruction-shaped text that reached it — model replies echoing fetched content, linter and tool output — returns in a later session with whatever origin framing it had stripped.confirmed PRAX-2026-08-12-011
B5 Local transcript and history files (data-at-rest) — 2 threats, 1 remit rules · worst: confirmed
R-15 partial Local chat-history files SHOULD be gitignored and owner-readable, with credential-pattern redaction recommended.
STRIDEOWASPThreatStatus
ILLM02The transcript is opened for append with no mode argument and there is no chmod anywhere in the scanned tree, so it lands world-readable under a typical umask, and nothing filters credential patterns that pass through the conversation into it.confirmed PRAX-2026-08-12-010
ILLM02--llm-history-file writes the full request and response stream, including every added file's contents, to a plaintext file with the same default permissions and no redaction (checked: io.py:754-765 opens it with no mode argument; grep for chmod/0o600 across the scanned tree returns nothing).potential
B6 Repository content to the model provider (model-egress) — 2 threats, 2 remit rules · worst: partial
R-11 verified Repository source, file contents, prompts, and credentials MUST NOT be transmitted to any destination other than the operator-configured LLM provider(s) required to perform the requested edits.
R-06 verified Only operator-configured provider endpoint(s); connections MUST verify TLS certificates.
STRIDEOWASPThreatStatus
ILLM02Prompts, added file contents and the repository map cross the network in the clear if certificate verification is turned off; verification is on by default.partial aider/args.py:151
remainder: a single --no-verify-ssl flag disables verification globally — the provider connection, the model-metadata fetch and the web scraper all drop certificate checking together
ILLM02Repository source could leave for a destination other than the sanctioned provider, but there is exactly one completion call site and the endpoint comes from the operator's own model and environment configuration; no other outbound path carries repository content.mitigated aider/models.py:1036
B7 Provider credentials at rest and in the process environment (secret-material) — 2 threats, 0 remit rules · worst: potential
the remit does not touch this boundary — threats here are assessed against the RAISE/OWASP baseline alone (a remit is a job description, not a security model; silence here is normal)
STRIDEOWASPThreatStatus
ILLM02Provider keys are loaded from .env files and the OAuth key file into the process environment, and the shell tool spawns with that environment inherited, so any approved command can read every configured key (checked: run_cmd.py:62-73 and :116 pass no env argument, and commands.py:972-983 sets only GIT_EDITOR).potential
ILLM02The OpenRouter OAuth flow appends the issued key to a file under the home directory opened with no mode argument, so it lands at the default umask (checked: onboarding.py:361-367; grep for chmod/0o600 across the scanned tree returns nothing).potential
B8 Runtime-fetched metadata and dependencies (supply-chain) — 2 threats, 1 remit rules · worst: confirmed
R-09 verified Integrity of what they return is still in scope (e.g., an unpinned metadata fetch is a supply-chain finding, not a trust expansion).
STRIDEOWASPThreatStatus
TLLM04The price and context-window database that drives model selection and token budgeting is pulled from the main branch of a third-party repository and cached with no signature or digest check, and the /help embedding model is downloaded by bare name with no revision pin.confirmed PRAX-2026-08-12-008
TLLM04A dependency version swap or confusion attack would reach the runtime, but all runtime dependencies are ==-pinned in a machine-compiled requirements file built against a shared constraints file.mitigated requirements.txt:1
B9 Browser UI reachable off-host (control-plane-exposure) — 2 threats, 1 remit rules · worst: confirmed
R-05 gap Experimental web UI; it MUST bind to loopback only and MUST NOT be exposed to a public or otherwise untrusted network.
STRIDEOWASPThreatStatus
SASI03Browser mode launches Streamlit with no --server.address and no authentication, so on a shared or untrusted network anyone who can route to the port is treated as the operator and can add files, drive the model and issue git and shell input.confirmed PRAX-2026-08-12-005
ELLM03The GUI constructs its command IO with yes=True, so confirmations raised by browser-issued slash commands answer themselves; the explicit-yes carve-out still refuses the model-suggested shell gate.partial aider/io.py:866
remainder: every gate not marked explicit_yes_required — edits to files not added to the chat, URL fetches, and command-output ingestion — auto-answers yes for commands issued through the browser
B10 Analytics and local audit record (telemetry-egress) — 3 threats, 2 remit rules · worst: confirmed
R-13 partial Analytics telemetry MUST NOT include source code, prompt/chat content, API keys or credentials, or personal information.
R-12 verified Analytics / PostHog telemetry is opt-in and OFF by default; aider MUST NOT send any analytics without explicit operator opt-in.
STRIDEOWASPThreatStatus
ILLM02The PostHog client is constructed with automatic exception capture enabled and no before-send scrubber, so uncaught exceptions ship stack frames carrying absolute paths from the developer's machine — the one payload class the deliberate redaction helper never sees.confirmed PRAX-2026-08-12-012
R—The two highest-impact actions — running a command and writing a file — produce no structured event with a timestamp, path, exit status or approval outcome; the only durable trace is a free-form markdown transcript.confirmed PRAX-2026-08-12-009
I—Telemetry could be collected without the developer's knowledge, but the client disables itself unless the user has been asked and opted in, and a permanent opt-out is persisted.mitigated aider/analytics.py:85
Component Inventory
Every component with its kind, lane, and source evidence — the diagram's tooltips, on paper.
ComponentKindLaneDescriptionEvidence
Local operator/developerentrypointuser_inputsThe developer who launched aider and types its instructions; the only trusted instruction source in the remit.aider/io.py:523; aider/coders/base_coder.py:876
Watched-file AI commentsentrypointuser_inputsFileWatcher turns AI / AI! / AI? comments found in any watched repository file into an instruction for the model, with no check on who authored the file.aider/watch.py:69; aider/watch.py:181
Untrusted network callerentrypointuser_inputsAny host that can route to the Streamlit port; browser mode ships with no bind address and no authentication.aider/main.py:233; aider/gui.py:374
Terminal CLI (InputOutput)clientclient_adaptersThe prompt_toolkit terminal surface: reads operator input, prints tool and model output, and owns the input/chat history files.aider/io.py:523; aider/io.py:995
Streamlit browser UIclientclient_adaptersExperimental web UI that drives the same Coder object; exposes chat, file add/drop, shell-command and git inputs.aider/gui.py:361; aider/gui.py:219
Slash-command dispatcheradapterclient_adaptersMaps /web, /run, /test, /git, /add, /undo, /help and friends onto the tools; also the surface that appends fetched and command output into the chat.aider/commands.py:312; aider/commands.py:219
Coder request looporchestratoragent_coreThe single-agent loop: assembles context, calls the model, parses edits and shell commands out of the reply, applies them, commits, lints and reflects. Chat-chunk plumbing, the per-format Coder subclasses' parsing and the history summarizer fold in here.aider/coders/base_coder.py:876; aider/coders/base_coder.py:1419
Prompt stack and content framingpromptagent_coreThe prefixes and templates that frame every piece of content sent to the model; the only framings available are for added files, the repo map and read-only files -- there is no untrusted-content framing.aider/coders/base_prompts.py:24; aider/prompts.py:36
Model call (Model.send_completion)modelagent_coreProvider-agnostic model call through litellm, plus the model-metadata cache that shapes model selection and token budgeting.aider/models.py:1036; aider/models.py:162
.aider.chat.history.md transcriptmemoryagent_coreThe durable markdown transcript aider appends to on every turn; readable back into a later session as prior conversation when --restore-chat-history is set.aider/io.py:1117; aider/args.py:274
confirm_ask approval gatecontrolagent_coreThe one approval primitive the whole codebase uses; under --yes-always it answers 'yes' to every gate except those marked explicit_yes_required, which it answers 'no'.aider/io.py:807; aider/io.py:866
Model-suggested shell gatecontrolagent_coreThe only call site in the tree that passes explicit_yes_required=True, making the shell-run confirmation immune to --yes-always; the sibling gate on ingesting the command output in the same function is an ordinary confirm_ask.aider/coders/base_coder.py:2456; aider/coders/base_coder.py:2479
File-edit gate (allowed_to_edit)controlagent_coreThe only authorization step on a model-chosen write path: checks gitignore and asks for confirmation, but never checks repo-root containment or .aiderignore, and is not marked explicit_yes_required.aider/coders/base_coder.py:2191; aider/coders/base_coder.py:2226
Auto-detected URL fetch gatecontrolagent_coreConfirms before fetching a URL spotted in input text; an ordinary confirm_ask with allow_never, so --yes-always answers it.aider/coders/base_coder.py:964; aider/coders/base_coder.py:976
Shell execution (run_cmd)tooltools_mcpExecutes command strings through the user's login shell with the inherited environment; the host-execution sink for /run, /test and model-suggested commands.aider/run_cmd.py:62; aider/run_cmd.py:116
Linter invocationtooltools_mcpRuns a per-language or operator-configured lint command after every edit; the filename argument is shell-quoted, but the invocation itself has no pre-execution confirmation.aider/linter.py:47; aider/linter.py:82
Web scrapertooltools_mcpFetches a URL with Playwright or httpx and converts the HTML to markdown; no domain allowlist and no content labelling.aider/scrape.py:98; aider/scrape.py:194
Git repository operationstooltools_mcpGitPython wrapper for staging, committing, diffing and undoing; also the owner of the .aiderignore / --subtree-only scope check.aider/repo.py:131; aider/repo.py:278
Repository maptooltools_mcpBuilds a ranked tag summary of the wider repository for context; sourced from git-tracked files filtered through the aiderignore scope, cached under the repo root.aider/repomap.py:103; aider/repo.py:486
Edit-format coders (file writer)tooltools_mcpThe family of edit-format coders enumerated in coders/__init__.py (edit-block, patch, whole-file, unified-diff and their editor variants); each implements apply_edits, which is where model output becomes bytes on disk.aider/coders/__init__.py:1; aider/coders/editblock_coder.py:41
/help documentation indextooltools_mcpA llama-index vector store built over aider's own packaged website markdown and cached under the home directory; retrieval feeds a help-mode coder. The corpus ships with the package, so its write path is not agent-writable.aider/help.py:84; aider/help.py:139
Fetched web pageexternal_serviceexternal_deployWhatever host the operator's URL resolves to; its content is attacker-authored from aider's point of view and reaches the model with no provenance marking.aider/commands.py:227; aider/scrape.py:170
Configured LLM providerexternal_serviceexternal_deployThe operator-configured provider endpoint litellm resolves from the model name and environment keys; receives prompts, added file contents and the repository map.aider/models.py:1036; aider/main.py:519
PostHog analyticslog_sinkexternal_deployOpt-in telemetry client; explicit event properties are redacted, but the client is constructed with automatic exception capture enabled.aider/analytics.py:102; aider/analytics.py:213
Extraction Notes
lane_fit: Three placements strained. The fetched web page is filed in external_deploy as an external service although its only role in this model is as an untrusted input origin, which user_inputs also claims. io.py spans two lanes: its terminal surface is client_adapters while its confirmation gate is filed in agent_core under the control-lane rule, because the orchestrator is the process that calls it and keeping it there keeps the control chain local. The chat transcript is filed as agent_core memory because it re-enters the agent's own reasoning, even though it is simultaneously the target's only durable log and could equally be read as a log sink.
omissions: Arbitration divergence: the stored finding PRAX-2026-08-12-004 carries ASI05 alongside LLM03; under the Agentic KB's compound table the more specific row is 'Missing/disabled approval gate on a consequential action', which makes LLM03 the primary and ASI05 a co-tag, so the tool-invocation threat is tagged LLM03. Tagging note: PRAX-2026-08-12-001 is stored LLM10 for the whole chain under the sink>input>grant order; the tool-invocation threat drawn from it names the auto-answering approval as its mechanism, which is grant evidence, so it is tagged LLM03 there — the sink half of the same chain is carried by the LLM10 threat backed by PRAX-2026-08-12-003 at the same boundary. Code-match scope: PRAX-2026-08-12-011 asserts the transcript 'receives scraped page text and command output verbatim via the cur_messages path'; in the snapshot the transcript writers are io.user_input (the raw typed line, logged before check_for_urls appends fetched content), io.ai_output (assistant replies) and io._tool_message (all tool_output/tool_warning lines, which do carry linter stdout via lint_edited) — cur_messages is not a writer to that file, so the stored-state threat is scoped to what the code supports and the finding is still cited. Components suspected but folded rather than given nodes: voice transcription (voice.py), clipboard/image paste (copypaste.py) and the OpenRouter OAuth onboarding flow (onboarding.py) are declared capabilities whose trust consequence at these boundaries is already carried by the ingest gates and the secret-material boundary; the per-format coder subclasses, chat-chunk assembly and the history summarizer are framework plumbing folded into the orchestrator and the memory node. Scope: aider/*.py top-level and aider/coders/ per the scan instructions, with requirements.txt cited only for the tree-wide dependency-pinning sweep; benchmark/, scripts/ and tests/ were not examined.
generated by: Opus 5 (1M context)