Skip to main content

Caging the Agent

·1087 words·6 mins
AI Security Me & My Agent
Table of Contents

Time for a rallying cry:

LLMs are non-deterministic. Don’t TRUST that shit!

So, while newer models are getting ridiculously good and helpful, we can’t just let them roam around freely. That is cause while they might be fucking great at what they doing… they actually don’t know what they’re doing.

That was also one of my major concerns while running Claude Code on my workstation. I could never quite get myself to simply accept all edits and command executions, but that also made the process slow and tedious, having to accept every single writing or executing access (and let’s be honest through all the accepting, at some point something slips through anyhow).

Mr. Meeseeks running whatever command with user privileges

And generally, having all that executions being triggered by the agent (be it go build, go test, curl) kept making my security Spidey senses flare up like a Christmas tree. While, yeah, I know it’s not something that should easily happen that some malicious script is just popping out of your favorite LLM, it’s basically our main job in Security to look into what could go wrong. And non-deterministically generated commands that might run in the context of my own user led me to the decision of making my Claude coding agent a… Cagent.

Ideally, we would just confine that … lil’ code tsunami into its own hermetically sealed environment. Since I don’t have extra environments lying around all the time and basically still want to have it run on my own workstation, I decided to limit it with its own dedicated low-privilege user.

Locking the agent into the context of its own user
#

We want to create its own user for our coding agent that is restricted in what it can do. No access to private files of the root user or any other users and writes restricted to their very own home directory. I’m calling the user for my agent meeseeks, but you can just name it whatever you want, be it claude, sandbox-user, crawl-the-warrior-king, whatever floats your boat.

# Create the user account
sudo dscl . -create /Users/meeseeks

# Beware to set the right shell here, that you then want your agent to run in

sudo dscl . -create /Users/meeseeks UserShell /bin/zsh
sudo dscl . -create /Users/meeseeks RealName "Mr. Meeseeks"
sudo dscl . -create /Users/meeseeks UniqueID 69
sudo dscl . -create /Users/meeseeks PrimaryGroupID 20
sudo dscl . -create /Users/meeseeks NFSHomeDirectory /Users/meeseeks
sudo dscl . -create /Users/meeseeks IsHidden 1

# Set a password (that is not your standard user's password)
sudo dscl . -passwd /Users/meeseeks 'comeUpWithYourOwnPassword'

# Create home directory for the new user
sudo createhomedir -c -u meeseeks

This creates a new user meeseeks with display name Mr. Meeseeks, User ID 69 as part of the staff group including a home directory where we’d expect it and including a fresh password. After switching to the new agent user’s context with su meeseeks, we can now install Claude Code or your agent of choice for the user.

Locking it up tight
#

Now we already got the coding agent having limited privileges per se running as its dedicated user that cannot access your personal user’s private files in your home directory. Still, there are some traditionally world-readable paths that we additionally might want to (and actually should) hide from the agent, just in case. You know, also for when the agent user might get compromised, as ya never know (the whole Claude family has been a bit leaky recently, just sayin’). So, some additional places where the agent simply has no business to put its nose into are:

  • Access to the /Users listing itself
  • Access to your standard user’s home directory
  • Access to system-sensitive paths such as /etc, /var or /Library/Keychains

There is different options to do that:

Explicit deny with chmod
#

That’s the one I went with. We use chmod to explicitly deny access to a couple of sensitive locations for the meeseeks.

sudo chmod +a "meeseeks deny read,write,execute,list" /Users/$(whoami)
sudo chmod +a "meeseeks deny read,write,execute,list" /Users
# ... and so on

# Or just loop it...

for dir in /etc /private/etc /private/var /Library/Keychains; do
  sudo chmod +a "meeseeks deny read,write,execute,list" "$dir"
done

Sandbox
#

Alternatively, we could use a sandboxed execution wrapper. For example, macOS has sandbox-exec which can limit a process to specific paths at the kernel level. A minimal profile would look like:

scheme(version 1)
(deny default)
(allow file-read* file-write*
    (subpath "/Users/meeseeks"))
(allow process-exec
    (subpath "/usr/bin")
    (subpath "/usr/local")
    (subpath "/Users/meeseeks"))
(allow network-outbound)
(allow sysctl-read)

Coop mode on
#

Depending on your way of working with the agent, this step is optional. If you like to live dangerously and let the agent roam fully independent, then you will be fine with giving it its own sandbox / playground in its own home directory, because it has full access there. But I prefer working directly in the mud and in tandem with my agent basically, meaning I got my IDE with the code open, so I can make minor adjustments there myself, review on the fly, all while having the agent on a series of individual tightly scoped tasks.

For this, your personal user needs write access to the Claude Code user home directory. This is easiest done by putting the personal user and the agent user into a shared group for collaboration (I called mine collabo, but obviously you can name it however you want).

Creating a group for collaboration
#

sudo dscl . -create /Groups/collabo
sudo dscl . -create /Groups/collabo PrimaryGroupID 69
sudo dscl . -append /Groups/collabo GroupMembership $(whoami)
sudo dscl . -append /Groups/collabo GroupMembership meeseeks

This creates the user group named collabo with the ID 69 and adds the current as well as the user named meeseeks to it.

Setting the group as the owner of the workspace
#

sudo chown -R meeseeks:collabo /Users/meeseeks/workshop
sudo chmod -R 2770 /Users/meeseeks/workshop

Now, group collabo is set as the owner group of the agent’s working directory /Users/meeseeks/workshop.

One more thing…
#

To make files created as your personal user group-writable, we need to add this to our ~/.zshrc:

umask 002

To avoid the same issue vice versa, it’s easiest to set up a Claude hook that will set the required permissions (chmod 664) for every file that has been touched by the agent via either Write or Edit:

"hooks": {
"PostToolUse": [
  {
    "matcher": "Write|Edit",
    "hooks": [
      {
        "type": "command",
        "command": "jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; [ -f \"$f\" ] && chmod 664 \"$f\"; } 2>/dev/null || true"
      }
    ]
  }
]
}

Fin