Coding Agents For The Paranoid
Having heard from several people positive reviews on coding agents, it was time to take them for a spin. However, I soon realized that the coding agents workflow is highly incompatible with my aversion to sharing unbounded data with LLMs.
Currently, we are living in an era when companies absolutely despise your privacy if it stands in their way and nobody seems to care. Moreover, the usually recommended installation via npm install -g raised another eyebrow in light of the recent npm supply chain attack.
By default, security is only an afterthought for most of the companies. Thus I highly prefer when the guarantees come from the design rather than being trust-based.
We are going to set up a local flow allowing us to productively run coding agents while having at least some assurance the agent is not doing anything shady. For those of you who want to skip the details of the setup, just check the repository with the final setup.
By the end, you’ll have Claude Code running in a Podman container with selective file mounting and persistent OAuth credentials. I will use claude but the principles should extend to any CLI-based coding agent.
The Silver Bullet
There are basically two major issues that make running the agent locally troublesome.
Firstly, while they claim their access is bounded to the working directory when you launch them, I find it hard to believe they won’t fail on such a promise. When running the agent, you are running closed source code whose code to be executed is effectively not reviewed by anybody1.
The agent developers might try their best to put up a wall the execution engine cannot cross but why even face such a risk? It seems that by running it locally, you are possibly giving access to the whole filesystem. Big no-no.
Secondly, the agent is able to execute binaries while running. I bet it’s very useful to make the whole flow more pleasant and productive. But again it’s kind of a bullet I’m just refusing to bite.
Playing it just for several days, I’ve seen way too many occurrences where the agent suggests to add unnecessary dependencies to the project. So even if it wasn’t malicious behavior on its side, it’s just too damn likely it slips and falls for a typosquatting attack.
Then it finally struck me that I can resolve both worries by simply running it in containerized isolation! It’s literally a four-liner to get you started:
FROM node:24-slim
RUN npm install -g @anthropic-ai/claude-code
WORKDIR /workspace
ENTRYPOINT ["claude"]
Riding the wave of paranoia, I’ve also decided to ditch docker in favor of podman2. Just build it and we’re ready to go:
podman build -t claude-code .
podman run -it --rm claude-code

Almost Production Ready
Soon after the initial excitement of having the first quick version ready, its limitations showed up. To make this actually useful, you need to mount the directory or files you want to expose to the agent. This turned out to be a longer journey than I wished for. I will save you from all the intermediate obstacles and give you the working version instead:
FROM ubuntu:24.04
RUN apt update && apt install -y nodejs npm
RUN npm install -g @anthropic-ai/claude-code
# Set up a non-root user with specific UID/GID
RUN groupadd -f -g 1234 claude && \
useradd -u 1234 -g 1234 -ms /bin/bash claude
WORKDIR /home/claude/project
USER claude
# Run claude as the default command
ENTRYPOINT ["claude"]
The primary issue was setting up volume mounts that allow both the container and host to read and write files. Rather than running as root, I switched to using ubuntu based image and configured a dedicated user. Feel free to try this setup yourself:
podman build -t claude-code .
podman run -it --rm --userns=keep-id:uid=1234,gid=1234 \
-v ./demo/tobeshared.txt:/home/claude/project/file.txt claude-code

Going full circle: users, groups and volume mounting
At first, running podman run -v <host/path/to/file>:<container/path/to/workdir/file> worked just fine. The problems started once I created dedicated user.
podman exec -it <container-id> bash-ing into the running container revealed the file permissions are misconfigured:
claude@88f672d518e7:~/project$ ls -alh
total 16K
drwxr-xr-x 2 root root 4.0K Oct 3 13:58 .
drwxr-x--- 1 claude claude 4.0K Oct 3 13:58 ..
-rw-r--r-- 1 root root 4.8K Oct 3 13:52 file.txtAt first, I thought the problem needs to be solved podman run -v so tried all the combinations described in the documentation. Having no success and a couple of back-and-forths with an LLM finally led me to discovering —userns .
Since we want write access from both host and container, UIDs need to be mapped properly.
The whole effort was severely hampered by something I discovered at the end. While I was playing with various options, writes from container to host were working mostly fine. But once I edited on the host, the mounted file started to behave like two separate files—the host and container each having their own version.
Looking at inode helped to pin point the root cause
# so far so good
claude@7b0919ebf1df:~/project$ ls -i file.txt
55849983 file.txt
[host]$ ls -i file.md
55849983 file.md
# writing from container via echo, still good
claude@7b0919ebf1df:~/project$ echo test >> file.txt
claude@7b0919ebf1df:~/project$ ls -i file.txt
55849983 file.txt
[host]$ ls -i file.md
55849983 file.md
# writing from container via echo, still good
[host]$ echo test-again >> file.md
[host]$ ls -i file.md
55849983 file.md
[host]$ tail -n2 file.md
test
test-again
claude@7b0919ebf1df:~/project$ tail -n2 file.txt
test
test-again
claude@7b0919ebf1df:~/project$ ls -i file.txt
55849983 file.txt
# writing from host using Neovim, inode has changed
[host]$ ls -i file.md
55861844 file.md
[host]$ tail -n3 file.md
test
test-again
written in Neovim
claude@7b0919ebf1df:~/project$ tail -n3 file.txt
test
test-again
The problem was Neovim’s default behavior writing to backup file and overwriting the original file on save. I ended up turning this feature off as my flow heavily utilizes single file mounts. This is likely only an issue when you mount a specific file—directory mounts should work fine.
The Last Few Miles
As I’ve been playing with the new flow, it has quickly become clear that going over OAuth
authorization on each podman run becomes boring very quickly.
Since I was running the agent locally before, I knew it is able to somehow cache the credentials and the caching is likely broken due to container isolation.
Reading through claude --help, I’ve noticed claude setup-token and my eyes lit as I was expecting finally easy exercise. After several iterations trying to pass the token to the container, ensuring it is part of the environment, the agent still refused to use it and asked for authorization. Only after reading this recent discussion, I concluded it is a dead end for now.
Since CLI storing their configuration in $HOME has become the industry standard, I played around locally and quickly discovered ~/.claude/.credentials.json together with .claude.json. Going through its content I also discovered its a cache of historical conversations stored per project3.
After my initial plan to cherry-pick only the needed bits, I settled with more general flow—run the agent once in an isolation, authorize via browser and let it bootstrap its files. Then COPY them to Docker image during the build. Since the token seems to be valid for a year, even the hassle of doing this regularly does not seem to be an issue
Tokens are valid for six hours by default, requiring regular container rebuilds. A helper script is available, with workflow improvements in development.
So we need to build the original simplified image, authorize via browser and copy the files from the container locally to later pass them to the final image:
podman build -t claude-code-credentials -f Dockerfile.credentials .
podman run -it --rm claude-code-credentials
# extract the credentials
podman cp <container-id>:/root/.claude.json ./credentials/.claude.json
podman cp <container-id>:/root/.claude/ ./credentials/.claude
# and build the final image
podman build -t claude-code .
Final touches to Dockerfile
FROM ubuntu:24.04
RUN apt update && apt install -y nodejs npm
RUN npm install -g @anthropic-ai/claude-code
# Setup proper user
RUN groupadd -f -g 1234 claude && \
useradd -u 1234 -g 1234 -ms /bin/bash claude
# Provide OAuth credentials
COPY credentials/ /home/claude/
# Required for credentials to be uptaken
RUN chown -R claude:claude /home/claude/
WORKDIR /home/claude/project
USER claude
# Run claude as the default command
ENTRYPOINT ["claude"]Voilà!
Finally Having Some Fun
You might be wondering why I’m so stubborn to mount just single file. My flow interacting with LLMs has been so far tediously copy pasting either code snippets or partial writings between the editor and the browser. With the setup above, I might finally level up my game.
I’ve been using Obsidian as my daily driver for personal notes. The implementation simplicity using markdown files made it possible to synchronize across multiple devices quite easily4. And as the files are stored locally, writing feels snappy and there are no outages5.
Recently, I’ve started to have fun with nushell and come up with this helper that lets me quickly open any note from my vault:

Modifying the snippet slightly gives us a powerful way to selectively feed context to the agent—ensuring only whitelisted files are exposed. The --multi flag even allows selecting multiple files or directories at once.
Easy peasy—just swap Neovim for Claude
let coding_agent_bind = {
name: claude-code
modifier: <modifier>
keycode: <keycode>,
mode: [vi_normal, vi_insert]
event: [
{
send: executehostcommand
cmd: '
let result = fd --type file --type directory --hidden |
sk --tmux=center,60% --multi --preview "bat --style=numbers --color=always {}" -p "Select LLM scope: ";
let mount_points = $result | split row "\n" | each { |item|
let base = (echo $item | path basename);
$"-v ./($item):/home/claude/project/($base)"
} | str join " ";
let command = $"^podman run -it --rm --userns=keep-id:uid=1234,gid=1234 ($mount_points) claude-code"
commandline edit -r $"($command) ";
commandline set-cursor --end;
'
}
]
}That was quite the adventure! I definitely overestimated how fast this would go. But hey, at least the end result delivered. Now I’m curious to see how well this actually works in practice.
Footnotes
-
Not talking about the code of CLI. The brain behind the execution is a billion parameter LLM whose intentions we are yet to understand ↩
-
To maximize the joy of playing with as many new toys as possible ↩
-
Check for yourself with
jq .projects[].history ~/.claude.json↩ -
Even though I’m fan of Notion in collaboration setup, you inevitably need to trust the company behind it with your data to enjoy its benefits. So for obvious reasons, I usually end up avoiding sharing the data whenever the operational costs are manageable. ↩