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.
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.
- Fetched web page The page is served by whatever host the URL resolves to — attacker-authored content, and aider ships no domain allowlist
- Web scraper [PRAX-2026-08-12-002] The HTML is converted to markdown with all text preserved and no trust labelling applied
- 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
- 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
- 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
- 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
- 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
- Streamlit browser UI [PRAX-2026-08-12-005] The page accepts a chat prompt and lets the caller add repository files to the session
- 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
- 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
- Edit-format coders (file writer) The model's replacement text is written into the developer's working tree
- 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
- 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
- Terminal CLI (InputOutput) process_changes returns the assembled comment text from get_input as though the operator had typed it
- 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
- 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
- Edit-format coders (file writer) [PRAX-2026-08-12-003] The edits land on disk with no operator in the loop at any point
B1 Untrusted content entering the model's context (untrusted-ingress) — 3 threats, 3 remit rules · worst: confirmed
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.
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| T | LLM01 | Scraped 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 |
| S | LLM01 | An 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 |
| E | LLM01 | The 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-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
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| E | LLM03 | Auto-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 |
| T | LLM10 | A 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 |
| E | LLM03 | The 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 |
| E | ASI05 | Shell 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-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.
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| T | LLM03 | Every 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
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| T | ASI06 | The 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
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| I | LLM02 | The 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 |
| I | LLM02 | --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-06 verified Only operator-configured provider endpoint(s); connections MUST verify TLS certificates.
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| I | LLM02 | Prompts, 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:151remainder: 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 |
| I | LLM02 | Repository 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
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| I | LLM02 | Provider 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 |
| I | LLM02 | The 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
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| T | LLM04 | The 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 |
| T | LLM04 | A 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
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| S | ASI03 | Browser 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 |
| E | LLM03 | The 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:866remainder: 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-12 verified Analytics / PostHog telemetry is opt-in and OFF by default; aider MUST NOT send any analytics without explicit operator opt-in.
| STRIDE | OWASP | Threat | Status |
|---|---|---|---|
| I | LLM02 | The 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 | Kind | Lane | Description | Evidence |
|---|---|---|---|---|
| Local operator/developer | entrypoint | user_inputs | The 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 comments | entrypoint | user_inputs | FileWatcher 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 caller | entrypoint | user_inputs | Any 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) | client | client_adapters | The 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 UI | client | client_adapters | Experimental 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 dispatcher | adapter | client_adapters | Maps /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 loop | orchestrator | agent_core | The 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 framing | prompt | agent_core | The 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) | model | agent_core | Provider-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 transcript | memory | agent_core | The 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 gate | control | agent_core | The 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 gate | control | agent_core | The 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) | control | agent_core | The 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 gate | control | agent_core | Confirms 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) | tool | tools_mcp | Executes 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 invocation | tool | tools_mcp | Runs 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 scraper | tool | tools_mcp | Fetches 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 operations | tool | tools_mcp | GitPython 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 map | tool | tools_mcp | Builds 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) | tool | tools_mcp | The 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 index | tool | tools_mcp | A 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 page | external_service | external_deploy | Whatever 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 provider | external_service | external_deploy | The 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 analytics | log_sink | external_deploy | Opt-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 |
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)