Hardening Against AI
When you delegate work to AI coding tools (Claude Code, Codex, Aider, Cursor, Gemini CLI, etc.), they run with your full user privileges. They can read, write, delete, push, force-push, and call any external API your shell can reach. This is by design — you want them to actually do work — but it means you need a defense strategy that does not rely on the AI behaving correctly.
This guide is AI-agnostic and tool-agnostic. It applies whether you use Karajan, raw Claude Code, raw Codex, or anything else.
In practice — from your agent
Section titled “In practice — from your agent”Once kj harden has run, the rules don’t live in a prompt your agent can forget — they live in git guards and the project’s policy. So when your agent (Claude Code, Codex, Antigravity, the VS Code assistant) tries to commit something that breaks a rule — no card, no review verdict, an AI-attribution footer, a change to a security-class rule — the commit simply doesn’t enter, and the reason is printed in the session. You don’t police the AI; the mechanism does. The one category that is never overridable, not even by you, is security.
Under the hood — try it yourself
Section titled “Under the hood — try it yourself”Harden a repo and read what it now enforces:
kj harden # installs the git guards + the Sentinel for this projectcat .karajan/policy.yml # what each role may do; which rules only warn vs denykj policy report # the standing rules, granted exceptions, and the sealed decision logA rule marked class: security closes without escape and without arbitration — a deny you cannot argue with is the point.
The wrong mental model
Section titled “The wrong mental model”“I’ll add
rm -rfto my deny list.”
This is whack-a-mole. For every dangerous command you block, the AI can find another:
| Blocked | Equivalent |
|---|---|
rm -rf |
find -delete |
find -delete |
python -c "shutil.rmtree(...)" |
python |
node -e "fs.rmSync(...)" |
node |
perl -e "unlink ..." |
You’d need to enumerate every shell command, every interpreter, every language’s filesystem API. You can’t.
The right mental model
Section titled “The right mental model”Defense in depth, with layers that don’t depend on enumerating bad behavior. The strong layers are kernel-enforced and server-side; everything else is reinforcement.
Layer 1 — Recovery: assume something will break
Section titled “Layer 1 — Recovery: assume something will break”Take regular encrypted backups to a destination you control. The goal isn’t “prevent damage” but “make recovery cheap and reliable”.
Recommended: BorgBackup to a local NAS, daily, with deduplication and AES encryption.
# Initialize repoborg init --encryption=repokey-blake2 user@nas:/path/to/borg-repo
# Daily snapshotexport BORG_PASSCOMMAND="cat ~/.config/borg/passphrase"borg create user@nas:/path/to/borg-repo::"$(date +%Y%m%dT%H%M%S)" \ ~/projects \ --exclude '**/node_modules' \ --exclude '**/dist' \ --exclude '**/.cache'
# Retention: 7 daily, 4 weekly, 6 monthlyborg prune user@nas:/path/to/borg-repo \ --keep-daily 7 --keep-weekly 4 --keep-monthly 6If an AI deletes something you needed, recovery is one borg extract away.
Layer 2 — Prevention: a separate restricted Linux user
Section titled “Layer 2 — Prevention: a separate restricted Linux user”This is the strongest layer. Create a dedicated user (e.g., ia-user) for running AI tools. The kernel itself enforces what the user can read and write.
Why it works: an AI running as ia-user cannot escape the UID. No find -delete, no Python script, no exotic syscall can bypass it. The kernel returns EACCES before the AI even attempts the operation.
# 1. Create usersudo useradd -m -s /bin/bash ia-usersudo passwd ia-user
# 2. Allow ia-user to traverse your home and read project dirssetfacl -m u:ia-user:--x /home/$USERsetfacl -R -m u:ia-user:r-X /home/$USER/projectssetfacl -dR -m u:ia-user:r-X /home/$USER/projects
# 3. Generate a separate SSH key for ia-user, register it on GitHubsudo -u ia-user ssh-keygen -t ed25519 \ -f /home/ia-user/.ssh/id_ed25519 -N ""gh ssh-key add /home/ia-user/.ssh/id_ed25519.pub \ --title "ia-user@$(hostname)"
# 4. Make claude (or any other CLI) discoverablesudo ln -s "$(which claude)" /usr/local/bin/claudePer-project workflow: grant write only when actively working, revoke after.
# Grant write on a projectsetfacl -R -m u:ia-user:rwX ~/projects/active-projectsetfacl -dR -m u:ia-user:rwX ~/projects/active-project
# Run AI tools as ia-usersudo -i -u ia-usercd /home/$YOUR_USER/projects/active-projectclaude # or codex, gemini, aider...One restricted user serves all AI CLIs. Claude Code, Codex, Aider, Gemini CLI, Cursor — they all run under the same UID and inherit the same restrictions. One configuration protects the whole ecosystem.
Layer 3 — Immutability for critical files
Section titled “Layer 3 — Immutability for critical files”For files that must never change (SSH keys, identity configs):
sudo chattr +i ~/.ssh/id_ed25519sudo chattr +i ~/.ssh/id_ed25519.pubsudo chattr +i ~/.ssh/configOnce set, even sudo rm -rf cannot remove the file. To modify legitimately, run sudo chattr -i <file> first.
Layer 4 — Server-side protection on Git hosts
Section titled “Layer 4 — Server-side protection on Git hosts”The remote should enforce its own rules independent of your local environment.
Branch protection rulesets (GitHub)
Section titled “Branch protection rulesets (GitHub)”Block force-push and deletion of main on every repo:
gh api -X POST repos/OWNER/REPO/rulesets --input - <<EOF{ "name": "Protect main", "target": "branch", "enforcement": "active", "conditions": { "ref_name": { "include": ["refs/heads/main", "refs/heads/master"], "exclude": [] } }, "rules": [ { "type": "deletion" }, { "type": "non_fast_forward" } ], "bypass_actors": []}EOFToken scopes
Section titled “Token scopes”Don’t grant delete_repo to your default GitHub token. Without that scope, no AI can delete your repos via the API even if it tries.
gh auth status # verify scopes; ensure delete_repo is NOT listedA token without admin power for the agent (ADR 0037)
Section titled “A token without admin power for the agent (ADR 0037)”Going further: the agent should not hold admin power at all. A classic or OAuth token with the repo scope lets it change branch protection, create environments and secrets, approve a protected environment and merge as admin, all in your name. An agent running as your OS user can read any token stored on your laptop, so a second account is not enough: the admin-capable token has to leave the laptop.
- Create a fine-grained token at
github.com/settings/personal-access-tokens/new, owned by your account. Give it Contents, Pull requests, Issues, Workflows and Actions (read and write), plus Commit statuses (read). Fine-grained tokens have no Checks permission; a PR’s checks are read through the permissions above. Give it no account permissions. Leave out Administration, Environments, Secrets, Variables and Deployments. - Load it into gh yourself, in your own terminal:
read -rs T && printf '%s' "$T" | gh auth login --with-token; unset T.read -shides what you type, and gh reads the token from stdin. Never paste it into the agent’s chat or into a command line. - Revoke the old admin-capable credentials: the GitHub CLI authorization under Settings, Applications, Authorized OAuth Apps (it invalidates gh’s old OAuth token), and any old classic token with the
reposcope under Settings, Developer settings, Personal access tokens, Tokens (classic). Review the other Authorized OAuth Apps too: an editor signed in to GitHub, such as VS Code, keeps arepo-scoped token on the laptop that the agent could read just the same. - Do admin acts (branch protection, environments, secrets, settings) in the browser with your passkey or 2FA.
- Keep “Do not allow bypassing the above settings” (
enforce_admins) on in the protected branch. The token still acts as you, and that setting is what stops an admin-role merge bypass.
kj doctor checks this with two probes that change nothing. It warns if the token gh uses can read branch protection (the Administration permission) or approve a protected environment (Deployments write).
Your SSH key stays for git and the agent can read it too. It only does git operations, never the API, and branch protection applies to it. For more, use a FIDO2 key (ed25519-sk), which needs a touch on every push.
Layer 5 — Tool-specific deny rules
Section titled “Layer 5 — Tool-specific deny rules”This is the weakest layer but useful for catching obvious mistakes.
Example for Claude Code (~/.claude/settings.json):
{ "permissions": { "deny": [ "Bash(rm -rf*)", "Bash(rm -fr*)", "Bash(git push --force*)", "Bash(git push -f*)", "Bash(git reset --hard*)", "Bash(git branch -D*)", "Bash(gh repo delete*)", "Bash(gh api -X DELETE*)" ] }}Layer 6 — Pre-commit hooks against AI attribution
Section titled “Layer 6 — Pre-commit hooks against AI attribution”If your policy is “no AI references in commits/PRs”:
- Local: pre-commit / commit-msg hook blocking
Co-Authored-By: Claude,generated by AI, etc. - Server-side: GitHub Action that checks PR body, issue body, commit messages, and review comments.
Reference: @geniova/git-hooks.
Recommended layer combination
Section titled “Recommended layer combination”| Layer | Effort | Effectiveness |
|---|---|---|
| Backups (Borg) | Low | High — recovery |
| Restricted user | Medium | Very high — kernel-enforced prevention |
chattr +i on SSH keys |
Trivial | High for those files |
| Branch protection | Trivial | High — server-side |
Token without delete_repo |
Trivial | High |
Common objections
Section titled “Common objections”“This is too much friction.” — Run AI as ia-user only for real delegation. For exploration, your normal user is fine. The friction is one sudo -i -u ia-user per session.
“My AI is trustworthy.” — Maybe today. The defense protects against future model updates, prompt injection from untrusted input, unaudited MCP servers, and your own 2am typos.
“I use Docker.” — Great Layer 2 alternative. Same principle: kernel-enforced isolation, not denylist policing.
Summary
Section titled “Summary”You can’t prohibit what you haven’t imagined. But you can deny access to what shouldn’t be touched.
The strong layers (kernel-enforced isolation + reliable backups) do the real work. Everything else is narrow adjustments. This applies whether you’re orchestrating agents with Karajan, running a single AI assistant, or any combination — the defenses live at the OS and Git-host level, which is exactly why they work.
Next: actual files to drop on disk
Section titled “Next: actual files to drop on disk”This page is the why. For the how — concrete config for git hooks, agent permissions (Claude / Codex / Gemini), and multi-account SSH — see Recommended Setup.
Karajan Code™ · AGPL-3.0