Running Claude Code Safely in Dangerous Mode
Claude Code's
--dangerously-skip-permissions flag is incredibly useful for automation. It
removes all confirmation prompts — file writes, shell commands, everything runs
without asking. But the name is honest: a misguided prompt could rm -rf your
home directory or push to the wrong branch.
I wanted the productivity of full automation without the risk of nuking my machine. The solution I landed on has two parts: a Lima VM for isolation, and a wrapper repo pattern for keeping my Claude configs version-controlled even when my team doesn't use Claude.
Note: The Lima setup in this post is specific to M series Macs. The vz
backend and Rosetta for Linux VMs are Apple-specific — they won't work on Intel
Macs or Linux hosts.
Here's the overall picture:
┌────────────────────────────────────────────────────────────────┐
│ macOS Host (ARM64 / Apple Silicon) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Lima VM (Ubuntu 24.04 / Apple vz) — ARM64 │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ Claude Code │ │ │
│ │ │ (--dangerously-skip-permissions) │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────────┐ ┌────────────────────────────┐ │ │ │
│ │ │ │ Wrapper Repo │ │ Rootless Docker │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ CLAUDE.md │ │ ┌──────────────────────┐ │ │ │ │
│ │ │ │ .claude/ │ │ │ Containers │ │ │ │ │
│ │ │ │ .mcp.json │ │ │ ARM64 or x86_64 │ │ │ │ │
│ │ │ │ │ │ │ (via Rosetta) │ │ │ │ │
│ │ │ │ repo/ │ │ └──────────────────────┘ │ │ │ │
│ │ │ │ ├── svc-a/ │ │ │ │ │ │
│ │ │ │ └── svc-b/ │ └────────────────────────────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ shared/ │ │ │ │
│ │ │ │ issues/ │ │ │ │
│ │ │ └──────────────┘ │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ mounts: [] ← no access to host filesystem │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ SSH keys, credentials, other projects — all safe │
└────────────────────────────────────────────────────────────────┘
Part 1: Isolating Claude in a Lima VM
Why Lima?
Lima gives you a lightweight Linux VM on macOS. Claude runs inside the VM with full permissions, but the blast radius is contained. If something goes wrong, you delete the VM and start fresh. Your host machine is untouched.
You might wonder why not just use Docker. The thing is, Claude itself often needs Docker — building images, running containers, docker compose. Running Docker-in-Docker adds complexity. With Lima, Docker runs natively inside the VM, and Claude can use it directly without socket mounting or privilege escalation tricks.
The Key Config Decisions
I wrote a Lima YAML config that provisions a complete Claude Code development environment. You can grab the full YAML here, but I'll walk through the important parts.
No Host Mounts
mounts: []
By default, Lima mounts your macOS home directory as read-only inside the VM. We disable this entirely. The whole point is isolation — if Claude can read your home directory, it can see your SSH keys, credentials, and other projects. An empty mount list means the VM is completely self-contained.
It is possible to mount your code from the host so you see the same files on
both sides, and there are scenarios where that's useful — for example, a shared
folder to drop images or reference files into the VM easily. But the file access
mechanism — virtiofs — is slow. Something like npm install writing thousands
of files to node_modules will take noticeably longer over a mount. If your
workflow involves heavy filesystem operations, keeping mounts empty and cloning
repos directly inside the VM is the better experience.
That said, if you connect to the VM via VS Code's Remote SSH, you can browse and edit files inside the VM with a familiar interface, and drag and drop files in when needed. There's some network latency for large files, but for day-to-day coding it works well enough.
Apple Virtualization Framework
vmType: 'vz'
vmOpts:
vz:
rosetta:
enabled: true
binfmt: true
We use Apple's native
vz backend, which
is the default on Apple Silicon (it's what
Docker Desktop uses too). It
has lower overhead than QEMU since it's integrated directly into macOS. The
Rosetta config is the key advantage here — it registers as a binfmt handler
inside the Linux VM, so any x86_64 binary (including linux/amd64 Docker
containers) runs automatically at near-native speed. Many Docker images are
still published as amd64-only, so this saves a lot of pain.
A note on Rosetta, because I was initially confused by this. Apple is phasing out Rosetta 2 on macOS — the translation layer that lets Intel Mac apps run on Apple Silicon. That's expected to be fully gone by macOS 28. But the Rosetta used here is a different thing: it's Rosetta for Linux VMs, part of Apple's Virtualization framework, which translates x86_64 Linux binaries inside ARM64 VMs. Whether this Linux VM Rosetta survives long-term is still an open question — Apple hasn't explicitly said. But for now it works, and it's what Docker Desktop uses under the hood for the same purpose.
Rootless Docker
The provisioning scripts install Docker but immediately disable the system daemon, then set up rootless Docker instead:
- mode: system
script: |
#!/bin/bash
set -eux -o pipefail
command -v docker >/dev/null 2>&1 && exit 0
export DEBIAN_FRONTEND=noninteractive
curl -fsSL https://get.docker.com | sh
systemctl disable --now docker
apt-get install -y uidmap dbus-user-session
- mode: user
script: |
#!/bin/bash
set -eux -o pipefail
systemctl --user start dbus
dockerd-rootless-setuptool.sh install
docker context use rootless
This rootless Docker setup is copied from
Lima's own Docker template.
Even inside the VM, there's no reason to run Docker as root. When Claude
executes a docker run command, the container processes run under the Lima
user's UID, not root.
Claude Code Installation
- mode: user
script: |
#!/bin/bash
set -eux -o pipefail
command -v claude >/dev/null 2>&1 && exit 0
curl -fsSL https://claude.ai/install.sh | bash
The native installer handles auto-updates, so you don't need to re-provision to get the latest version.
Playwright MCP
I use the Playwright MCP server
inside the Lima VM so Claude can interact with web pages. One thing to note: you
need to use Chromium, not Chrome. Playwright's Chrome stable build
doesn't support Linux ARM64
— the install script explicitly errors out on aarch64. Chromium works fine
though, and Playwright falls back to it automatically on ARM64 Linux.
Health Probes
The config includes probes that wait for Docker's rootless daemon and the Claude
binary to be ready before Lima reports the VM as "started." Without these,
limactl start would return immediately while things are still installing.
Day-to-Day Usage
# Create and start
brew install lima
limactl create --name=claude-dev claude-dev.yaml
limactl start claude-dev
# Enter the VM
limactl shell claude-dev
# Run Claude and log in (if using a Max subscription)
alias ccd="claude --dangerously-skip-permissions"
ccd
# then use /login inside the session
# When things go sideways
limactl delete claude-dev # start fresh
Since the VM is isolated from your host, you'll need to set up SSH keys inside the VM independently and add them to GitHub (or wherever your repos live). The VM doesn't inherit your host's SSH config or keys — that's by design.
The nice thing about this setup is that it's disposable. If Claude makes a mess, you delete the VM and recreate it in a few minutes. No damage to your host.
Part 2: Version-Controlling Claude Configs
Now for the second problem. I use Claude Code across multiple repositories at
work, but my team doesn't. I needed a way to keep my Claude configuration —
CLAUDE.md context files, custom skills, MCP server configs —
version-controlled and organized, without polluting the team's repos with files
they don't use.
Why Not Git Submodules?
Git submodules were the obvious answer for nesting repos. But submodules create a diff in the parent repo every time the submodule's commit pointer changes. Since these repos update independently and frequently, I'd constantly have uncommitted submodule pointer changes cluttering my git status. I wanted the repos inside my workspace directory, but completely decoupled from the wrapper's git history.
The Wrapper Repo Pattern
The idea is simple: create a wrapper git repo that contains your Claude configs at the root, and nest the actual code repositories inside it. The inner repos are gitignored — not submoduled — so they maintain their own independent git histories without creating noise in the wrapper.
my-project/ # Wrapper repo (its own git repo)
├── .gitignore # Ignores repo/ entirely
├── .claude/ # AI assistant config (version controlled)
│ └── skills/
├── .mcp.json # MCP server configurations
├── CLAUDE.md # Cross-repo AI context
├── my-project.code-workspace # VS Code workspace file
│
├── repo/ # Gitignored - not tracked by wrapper
│ ├── service-core/ # Standalone git repo (own remote)
│ └── service-integrations/ # Standalone git repo (own remote)
│
├── shared/ # Drop zone for reference files
│
└── issues/ # Cross-repo issue tracking
└── 316-feature-name/
└── README.md
The .gitignore is dead simple:
repo/
That single line is what makes this work. The wrapper repo tracks your Claude configuration. The actual code repositories manage themselves.
Why This Matters for Claude
Claude Code operates within a directory scope — it reads and modifies files within its working directory and its children. By nesting repos inside the wrapper:
- Claude sees everything. All codebases, documentation, and notes are within scope. It can cross-reference one service's API client with another service's route definitions.
- Context files live at the root.
CLAUDE.mdprovides project-wide context — how to build, test, and navigate the codebase. One file covers all repos because it sits above them. - Your team's repos stay clean. No
CLAUDE.mdfiles, no.claude/directories, no MCP configs in repos where the rest of the team doesn't use Claude. Your AI setup is entirely in the wrapper.
The Supporting Folders
shared/ is an informal drop zone. When I need Claude to reference
something from an external system — a Confluence export, a Slack thread
screenshot, an API spec — I drop it here. No structure enforced, files come and
go.
issues/ maps to GitHub issue numbers. Each folder has a README with
research findings, implementation plans, and architecture decisions. When I
start a new Claude session and say "work on issue 316," it reads the folder and
picks up context without me re-explaining everything.
The VS Code Workspace File
{
"folders": [{ "path": "." }],
"settings": {
"git.scanRepositories": ["repo/service-core", "repo/service-integrations"]
}
}
The git.scanRepositories setting tells VS Code to detect the inner repos as
separate git repositories, so you get proper source control for each one in the
sidebar.
Setup
mkdir my-project && cd my-project
git init
mkdir repo
git clone git@github.com:org/service-core.git repo/service-core
git clone git@github.com:org/service-integrations.git repo/service-integrations
echo "repo/" >> .gitignore
mkdir -p shared issues .claude/skills
Adding a new repo is just a git clone into repo/. No submodule commands, no
pointer commits.
Trade-offs
This isn't free. You lose the ability to pin repos at exact commits like submodules provide. New developers need to clone multiple repos (a setup script helps). And the wrapper repo doesn't guarantee which branches the inner repos are on.
But if you're working across a handful of related repos, you use AI coding tools, and you want your AI config version-controlled without affecting your team's workflow — this pattern works well.
Things to Note
The Lima VM isolates the filesystem — Claude can't touch your host machine's files, credentials, or other projects. That's the main win. But isolation has limits.
Once you set up SSH keys inside the VM and add them to GitHub, Claude has push access to your repositories. A bad or poisoned prompt could still force-push to a branch, open a bogus PR, or push sensitive data to a public repo. The VM protects your local machine, but it doesn't protect your remote services from actions Claude takes through authenticated tools like git and SSH.
Network access is also unrestricted — Claude can make HTTP requests, install packages, and reach any service the VM can reach. If you're running this on a corporate network, the VM has the same network access as your host.
In short: the VM is a blast radius reduction, not a full sandbox. It's a
significant improvement over running --dangerously-skip-permissions directly
on your Mac, but it's not airtight. Be thoughtful about what credentials and
access you give the VM, and keep an eye on what Claude is doing with them.
Wrapping Up
These two patterns solve different but related problems. The Lima VM gives you a safe sandbox to let Claude run wild without risking your host machine. The wrapper repo gives you a place to keep your Claude setup organized and version-controlled, even when you're the only one on the team using it.
Neither approach is particularly clever. The VM is just containment, and the wrapper repo is just putting files where they need to be. But sometimes the simple solutions are the ones that actually stick.