donttouch
(sort of) guards files from AI coding agents
donttouch puts a lightweight guardrail around files, to keep AI coding assistants from modifying them.
Think of it as a sign and velvet rope around the files. It should be a clear signal to any AI agent: “Do not change this file.”
…but they can get around it. This should not be replacing code reviews or human oversight. It’s just an experiment as an extra layer of protection and perhaps more importantly a stronger way to signal the invariants around files.
- Filesystem permissions — Makes files read-only via
chmod - Git hooks — Blocks commits and pushes that touch protected files
- Agent instructions — Injects rules into agent config files (CLAUDE.md, .cursorrules, etc.)
Why?
AI coding agents are powerful but sometimes overeager. They might:
- Reformat your carefully crafted config files
- “Fix” code you intentionally wrote a certain way
- Modify documentation you maintain by hand
- Touch infrastructure files that shouldn’t change casually
donttouch gives you a simple way to draw a line: these files are off-limits.
Key Features
- Works everywhere — Git repos and plain directories
- Pattern-based — Protect files with glob patterns (
*.toml,migrations/**) - Agent-aware — Injects instructions into Claude, Cursor, Copilot, and Codex config files
- Git-integrated — Pre-commit and pre-push hooks with optional Husky support
- Safe by design —
unlockanddisablemust be run from outside the project directory, so agents inside the repo can’t bypass protection
Quick Example
cd my-project
donttouch init # Interactive setup
donttouch lock # Make protected files read-only
donttouch inject # Add rules to agent config files
donttouch status # See what's protected
To edit protected files (from outside the project):
cd ..
donttouch unlock ./my-project
# make your changes
cd my-project
donttouch lock
Note: unlock also disables protection, so pre-push hooks will block until you lock again. This prevents accidentally pushing with protection off.
Installation
From Source (Cargo)
cargo install donttouch
From GitHub
git clone https://github.com/sksizer/donttouch
cd donttouch
cargo install --path .
npm (coming soon)
npx donttouch init
The npm package will download the appropriate binary for your platform.
Verify
donttouch --version
Quick Start
1. Initialize
cd your-project
donttouch init
This starts an interactive wizard that:
- Creates
.donttouch.tomlwith your file patterns - Optionally locks files immediately
- Offers to install git hooks (if in a git repo)
- Offers to inject agent instructions
2. Protect Files
Define patterns in .donttouch.toml:
[protect]
enabled = true
patterns = [
"*.toml",
"migrations/**",
"README.md",
]
Then lock them:
donttouch lock
3. Verify
donttouch status
Shows all protected files, their lock state, git hook status, and context.
4. Check Protection
donttouch check
Returns exit code 0 if all files are properly locked, non-zero otherwise. Use in CI or git hooks.
Working With Protected Files
When you need to edit a protected file, unlock from outside the project:
cd ..
donttouch unlock ./your-project
Make your changes, then re-lock:
cd your-project
donttouch lock
The outside-directory requirement is the key security feature — AI agents running inside your project directory cannot unlock files.
Configuration
donttouch uses a TOML config file: .donttouch.toml
File Format
[protect]
enabled = true
patterns = [
"*.toml",
"Cargo.lock",
"migrations/**",
".env",
"README.md",
]
Fields
[protect]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Whether protection is active |
patterns | string[] | [] | Glob patterns for files to protect |
Patterns
Patterns use standard glob syntax:
| Pattern | Matches |
|---|---|
*.toml | All .toml files in the root |
migrations/** | Everything under migrations/ |
docker-compose.yml | Exact file |
*.lock | All lock files |
Patterns are resolved relative to the project root (where .donttouch.toml lives).
Self-Protection
The .donttouch.toml file itself is always protected when you run lock. This prevents agents from modifying the config to remove patterns.
Enabled Flag
When enabled = false:
checkpasses (no enforcement)- Pre-push hook blocks (forces you to re-lock before pushing)
The flag is managed automatically:
donttouch locksetsenabled = truedonttouch unlocksetsenabled = false
Locking & Unlocking
How Locking Works
donttouch lock sets filesystem permissions to read-only on all files matching your patterns and marks protection as enabled in the config. Execute bits are preserved (so scripts stay executable).
# Before lock
-rw-r--r-- config.toml
-rwxr-xr-x deploy.sh
# After lock
-r--r--r-- config.toml
-r-xr-xr-x deploy.sh
The .donttouch.toml config file is also locked to prevent agents from modifying protection rules.
Lock
donttouch lock
No flags needed. Locks all files matching patterns in .donttouch.toml and sets enabled = true.
Unlock
# Must be run from OUTSIDE the project directory
cd ..
donttouch unlock ./my-project
Restores write permissions on all protected files, the config file, and sets enabled = false. This means git hooks (pre-push) will block pushes until you re-lock — preventing you from accidentally pushing with protection turned off.
Why Outside-Only?
AI coding agents execute commands from within your project directory. By requiring unlock to be called from outside, agents physically cannot bypass protection — even if they try to run the command, the canonical path check will reject it.
This also prevents symlink and path traversal tricks (../project, /proc/self/cwd, etc.) thanks to std::fs::canonicalize().
Typical Workflow
# Unlock from outside the project
cd ..
donttouch unlock ./my-project
# Make your changes
cd my-project
vim config.toml
# Re-lock when done
donttouch lock
Git Integration
donttouch detects git repos automatically and offers enhanced protection through hooks.
Hooks
Two hooks are available:
Pre-commit
Runs donttouch check before each commit. Blocks the commit if:
- Any protected file has been staged for commit
- Any protected file is not read-only
Pre-push
Runs donttouch check-push before each push. Blocks the push if:
- Protection is disabled (
enabled = false)
This prevents you from pushing code while protection is turned off.
Installing Hooks
Via init
The interactive donttouch init wizard offers to install hooks automatically.
Husky Support
If you use Husky, donttouch detects the .husky/ directory and installs into your existing Husky hooks rather than overwriting .git/hooks/.
Manual
You can add to your git hooks manually:
# .git/hooks/pre-commit (or .husky/pre-commit)
donttouch check
# .git/hooks/pre-push (or .husky/pre-push)
donttouch check-push
Plain Directory Mode
Use --ignoregit to skip git detection entirely:
donttouch init --ignoregit
This forces plain directory mode even inside a git repo — no hooks, no staged file checking.
Hook Cleanup
donttouch remove cleanly removes hook entries without destroying other hook content. It only removes the lines it added.
Agent Instructions
donttouch can inject rules directly into AI agent configuration files, telling agents not to modify protected files.
Supported Agents
| Agent | File | Behavior |
|---|---|---|
| Claude Code | CLAUDE.md | Appends if file exists |
| OpenClaw / Custom | AGENTS.md | Appends if file exists |
| Cursor | .cursor/rules/donttouch.mdc | Creates file |
| Codex | codex.md | Appends if file exists |
| GitHub Copilot | .github/copilot-instructions.md | Appends if file exists |
Usage
donttouch inject
Preview without writing:
donttouch inject --dry-run
What Gets Injected
Each file gets a block wrapped in markers:
<!-- donttouch:managed -->
## Protected Files (donttouch)
The following files are protected by donttouch and must not be modified:
- *.toml
- migrations/**
Do not edit, move, rename, or delete these files.
<!-- /donttouch:managed -->
Idempotency
Running inject multiple times is safe. The <!-- donttouch:managed --> markers are checked — if the block already exists, it’s updated in place rather than duplicated.
Cleanup
donttouch remove strips the injected blocks from all agent files.
CI / GitHub Actions
Using donttouch check in CI
Add a step to your CI pipeline to verify protected files haven’t been modified:
- name: Check protected files
run: |
cargo install donttouch
donttouch check
donttouch check exits with code 0 if all protected files are read-only and no staged changes affect them. Non-zero means a violation.
Planned: donttouch ci
A dedicated donttouch ci command is planned that will:
- Compare the PR diff against protected patterns
- Fail the check if any protected file was modified
- Work without needing file permissions (useful in CI containers)
Stay tuned — this is on the roadmap for v0.6.
CLI Commands
donttouch init
Interactive setup wizard. Creates .donttouch.toml, optionally locks files, installs hooks, and injects agent instructions.
Flags:
--ignoregit— Force plain directory mode (skip git detection)
donttouch lock
Set protected files to read-only and set enabled = true in config. Also locks .donttouch.toml.
donttouch unlock <target>
Restore write permissions on protected files and set enabled = false. Must be run from outside the target directory.
Pre-push hooks will block pushes until you re-lock.
Arguments:
target— Path to the project directory
donttouch check
Verify all protected files are read-only. In git context, also checks that no protected files are staged.
Exit codes:
0— All good1— Violation found
donttouch check-push
Verify protection is enabled. Used in pre-push hooks.
Exit codes:
0— Protection enabled1— Protection disabled
donttouch status
Display current state: patterns, matched files, lock status, context (git/plain), and hook status.
donttouch inject
Inject protection instructions into agent config files.
Flags:
--dry-run— Preview what would be written without making changes
donttouch remove <target>
Full cleanup: unlock files, remove config, uninstall hooks, strip agent instructions.
Must be run from outside the target directory.
Arguments:
target— Path to the project directory
Config File Reference
Location
.donttouch.toml in the project root.
Full Example
[protect]
enabled = true
patterns = [
"*.toml",
"Cargo.lock",
"migrations/**",
".env",
".env.*",
"README.md",
"LICENSE",
"docker-compose.yml",
]
Schema
[protect]
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | bool | No | true | Whether protection is active |
patterns | string[] | Yes | [] | Glob patterns relative to project root |
Notes
- The config file itself is always protected when locked
- Patterns use standard glob syntax
- Paths are relative to the directory containing
.donttouch.toml
How It Works
donttouch uses three layers of defense to protect files from AI coding agents.
Layer 1: Filesystem Permissions
The primary mechanism. donttouch lock removes write bits from protected files using chmod. This is a hard enforcement — any process (including AI agents) will get a “permission denied” error when trying to write.
Execute bits are preserved so scripts remain runnable.
Layer 2: Git Hooks
In git repositories, donttouch installs hooks:
- Pre-commit: Runs
donttouch checkto verify no protected files are staged and all are read-only - Pre-push: Runs
donttouch check-pushto block pushes when protection is disabled
Hooks integrate with Husky if present.
Layer 3: Agent Instructions
donttouch inject writes rules into agent configuration files. This is a soft enforcement — agents that respect their instruction files will avoid protected files even before hitting the permission wall.
The Outside-Directory Rule
The critical security property: unlock and remove must be called from outside the project directory. Since AI agents execute from within the project, they cannot bypass protection.
This is enforced via std::fs::canonicalize() on both the current working directory and the target path, preventing symlink tricks and path traversal.
State Machine Architecture
Internally, donttouch uses an enum-based state machine for all program flow. Each command is a sequence of state transitions:
Start → ToInit → Initializing → EndInit → OfferHooks → OfferInject → Done → End
This makes the flow explicit, testable, and easy to extend.
Contributing
Building
git clone https://github.com/sksizer/donttouch
cd donttouch
cargo build
Running Tests
cargo test
Code Quality
cargo fmt --check
cargo clippy -- -D warnings
CI
Pull requests run checks automatically via GitHub Actions:
cargo checkcargo fmt --checkcargo clippy -- -D warningscargo test- Cross-platform build (Linux, macOS, Windows)
Architecture
Single-File Design
donttouch is currently a single-file Rust application (src/main.rs). This keeps things simple for a focused CLI tool.
State Machine
All program flow uses an enum-based state machine:
#![allow(unused)]
fn main() {
enum State {
Start,
ToInit,
Initializing,
EndInit,
OfferHooks,
OfferInject,
Done(String),
Error(String),
End,
}
}
Each state’s run() method performs its work and returns the next State. The main loop drives transitions until End is reached.
Context
The tool detects its operating context:
#![allow(unused)]
fn main() {
enum Context {
Plain,
Git {
has_husky: bool,
hooks_installed: bool,
},
}
}
Git context enables hooks and staged-file checking. Plain context uses filesystem permissions only.
Key Design Decisions
- Filesystem-first — chmod is the primary enforcement, git is layered on top
- Outside-directory rule —
canonicalize()prevents agents from bypassing protection - Config self-protection —
.donttouch.tomlis always locked with protected files - Idempotent inject — Marker comments prevent duplicate agent instructions
- Husky-aware — Detects and integrates with existing Husky setups
Dependencies
clap— CLI argument parsingglob— File pattern matchingtoml/serde— Config file parsing- Standard library for filesystem operations