Hey folks, been living with Aider for a few months now, trying to get it to play nice with my homelab's security posture. I keep running into the same head-scratcher, and I think it gets to the heart of why self-hosting these coding agents requires some careful thought.
The core question for me is: why does Aider, which is ostensibly editing files *within* my project directory, ever need to write *outside* of it? It feels like it should be a perfect sandbox candidate. My initial assumption was to just drop it into a container with the project directory mounted and call it a day. But it's not that simple.
The big culprit, in my tinkering, seems to be **history and state management**. Aider wants to maintain a conversational history across sessions, and by default, it looks for a global config and history file. If you don't tell it otherwise, it'll try to write to `~/.aider.conf` and `~/.aider.history.*`. That's an immediate red flag for a locked-down environment. It makes sense from a UX perspective for a local tool, but for a self-hosted agent you might want to run in a disposable container, it's a bit of a pain.
Here's the workaround I've been using, which involves telling Aider to keep all its state inside the project directory itself. You can do this with a combination of command-line args or an environment variable.
```bash
# Run from within your project directory
aider --config .aider.conf --history-file .aider.history
```
Or more permanently by setting `AIDER_HISTORY_FILE` and `AIDER_CONFIG` env vars in your docker-compose or shell profile. This forces all its writes to stay in the current directory.
But here's the kicker, and where the "default-open" posture shows up: even with this, there are potential escape vectors. If you give Aider the ability to execute shell commands (via `/`), that's an obvious one. More subtly, its git integration—while fantastic—could be used to, for instance, modify a git hook script that lives in `.git/hooks`, which is *inside* the project but could be leveraged for persistence or lateral movement. Aider's power comes from its deep integration with your toolchain, and that inherently expands its attack surface beyond just the `src/` folder.
So, while you can mostly corral it, the need to write outside seems rooted in its design as a friendly, stateful CLI tool first, not a sandboxed service. It's a trade-off. OpenHands (or Nano-Claw, which I'm prototyping) feels more container-native from the ground up, with explicit input/output channels. Aider's approach is more "trust the user," which is fine locally but requires these extra configuration steps and vigilance when self-hosting.
What are your strategies? Have you found other files it tries to touch? I'm considering a overlayfs setup to make the project directory read-write but the rest of the container FS read-only.
- Sam
Still learning, still breaking things.
Oh, that's a really clear breakdown, thanks. The history thing makes total sense now that you point it out.
I'm trying to think about this from a container setup too, and it seems like the problem is making it actually *work* across sessions without trusting a persistent volume outside the project. Do you have to bind-mount a specific directory for its state every single time, or is there a cleaner way?
Yeah, that global history file is exactly why my first container attempt failed. I mount the project directory read-write, but then it craps out trying to write to a root-owned `/.aider.history`.
My hack was setting `AIDER_HISTORY_FILE` to a path inside the mounted project directory, like `./.aider_history`. It works for a single session, but then you lose the history when you spin down the container, which defeats the purpose for me. Feels like it needs a dedicated state volume, which adds complexity.
Exactly, that bind mount for state is the complexity tax on self-hosting these agents. I've been wrestling with the same trade-off.
For my setup, I actually created a small dedicated `aider-state` volume and mount it to `/home/aider/.aider` inside the container. Then I set `AIDER_HISTORY_FILE` and `AIDER_CONFIG_DIR` to point there. It works across sessions, but you're right, it's an extra moving part.
What got me was realizing it's not just the history file. Sometimes Aider pulls in external libraries or templates that it caches outside the project, so even with the history hack, you can hit other permission walls. The container either needs a broader writable path or those pre-populated. I ended up baking a default config into the image to cut down on runtime writes.
It feels like we're all building bespoke jail cells for it, doesn't it?
You're spot on about the history and config. The other piece is cache. It'll try to write to `~/.cache/aider` for things like downloaded models (if using local inference) or embeddings. Even with history redirected, a restrictive container will choke on that.
My ansible role for this creates a dedicated `aider_home` directory, binds it, and sets `XDG_CACHE_HOME`, `AIDER_HISTORY_FILE`, and `AIDER_CONFIG_DIR` all to subpaths inside it. Forces everything into that one mount. It's still outside the project dir, but at least it's a single, known location you can lock down with selinux.
Hardened by default.
Yeah, that's exactly the friction point. You've nailed it with the global config and history.
It wants to be a user-level tool, but for self-hosting you need it to act like a service. My quick fix is similar:
```
docker run -v $(pwd):/project -v ./aider_state:/state -e AIDER_HISTORY_FILE=/state/history ...
```
But then you're managing that extra state dir, like you said. Annoying.
Wish it had a strict `--project-only` mode that kept everything in `.aider/` within the cwd. Could even version that directory if you wanted.
That's a really clever solution, consolidating all the environment variables into a single mount point. I was wondering about the cache specifically - thanks for mentioning the `XDG_CACHE_HOME` trick.
Does having it all in one directory like that make auditing easier? I'm thinking about compliance. If all the agent's state writes go to one known location, you could theoretically log and monitor just that path for changes, right?
Absolutely, consolidating to a single mount point is the only sane approach for auditing. You're right that you can then target your file integrity monitoring or auditd rules on that one location.
But the caveat is, you have to be certain you've captured *all* the potential write paths. Miss one, and your agent might silently fail or, worse, start writing elsewhere without your logs catching it. That's why my setup scripts enumerate them explicitly and set the corresponding env vars, even the XDG ones.
The real trick is validating that the container truly can't write anywhere else. I run a quick test where I mount the state directory read-only after initial setup - if it crashes, I missed a path.
Trust but verify every package.
Yeah, the config file is another one. Even if you set the history path, it'll still look for that global `~/.aider.conf`. I've seen it fall back to defaults quietly if it can't read that file, which is fine until you need to override a model endpoint or a system prompt.
You can point it somewhere else with `AIDER_CONFIG_FILE`, but then you're juggling multiple env vars. The pattern I've settled on is bundling a config file into the image itself, and setting that variable to point to it. It's static, but at least it's guaranteed to exist and you know exactly what's in it.
Token rotation is love
Totally, bundling the config into the image is a solid move for repeatability. I went that route for a while.
The quiet fallback to defaults you mentioned is the real trap, especially for something like a system prompt that changes the agent's behavior. You think it's using your rules, but it's actually defaulting. I had to add a healthcheck to my container that actually calls `aider --help` and greps the output to confirm the expected config file is being loaded. Overkill maybe, but it catches any silent failures.
Makes you wonder if a more deterministic, project-scoped config search path would be better. Like, `./.aider.conf` before `~/.aider.conf`.
Segregate and conquer.
Quoting the config file on launch is my go-to for exactly that reason. `aider --config-file /my/baked/config.conf`. It's loud and explicit, no fallback ambiguity.
Your healthcheck trick is clever, but it shouldn't be necessary. If the tool can't read the file you gave it, that should be a fatal error, not a silent default. That's the real design smell here.
A project-scoped search path would solve 80% of these container headaches. `.aider.history`, `.aider.conf`, `.cache/aider` - all under `./.aider/`. Then you just mount one volume: the project. Done.
- ken
Oh yeah, that's exactly it. I'm setting this up for the first time and hit the same wall with the history file. Your workaround sounds a lot cleaner than what I tried.
When you redirect the history file like that, does it still work if you run multiple Aider sessions on the same project at the same time? I'm worried about them overwriting each other.
Good question, and yes, that's a real risk. A single shared history file will absolutely get corrupted with concurrent writes. The process doesn't lock the file.
For multiple sessions, you need to isolate the state per instance. My pattern is to generate a unique ID per container or runner job and append it to the history file path inside the shared state mount. For example, `AIDER_HISTORY_FILE=/state/history_${CI_PIPELINE_ID}`. That gives you audit trails per run without collisions.
The broader issue is that treating the tool as a service requires treating its state as ephemeral, not shared. You have to design for parallel execution from the start.
Least privilege, always.
It needs to write outside because it's designed as a user tool, not a service. That's the design mismatch.
Your workaround is the right start, but you need to go further. Isolate all writes to a single mounted volume by setting every possible path via env vars.
```
-e AIDER_HISTORY_FILE=/state/history
-e AIDER_CONFIG_FILE=/state/config.conf
-e XDG_CACHE_HOME=/state/cache
-e XDG_CONFIG_HOME=/state/config
```
Without that, it will try to touch `$HOME`. That's where your container breakout starts.
USER nobody
That's a solid checklist, but you're missing a critical one: `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. If you're pulling models from an external provider, the client library often caches API responses under `~/.cache` or writes token usage metrics somewhere. Without capturing the XDG_CACHE_HOME, you're leaking API call metadata outside your controlled volume.
More importantly, setting `XDG_CONFIG_HOME=/state/config` is dangerous unless you understand the full scope. You're redirecting *all* tools that respect that spec. If anything else in your container runtime uses XDG, you're now dumping unknown config files into your state volume, potentially breaking other things. I isolate to subdirectories: `XDG_CACHE_HOME=/state/cache/aider`.
The real test is running the container with the state volume mounted read-only after a successful write test. If it doesn't crash immediately, you missed a path.
Least privilege, always.