Skip to content

justi/claude-code-project-boundary

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Project Boundary — Claude Code Plugin

Allows destructive operations within your project but blocks them outside the project directory. Built for --dangerously-skip-permissions mode where Claude doesn't ask — this plugin is your safety net.

How it differs from existing plugins

  • claude-code-safety-net — blocks rm everywhere; Project Boundary allows it inside the project so refactoring works normally.
  • destructive-command-guard — only distinguishes /tmp vs everything else; Project Boundary uses $CLAUDE_PROJECT_DIR as the actual boundary.
  • claude-code-damage-control — requires manually listing protected paths; Project Boundary automatically protects everything outside the project.

What it does

Boundary-checked (allowed inside project, blocked outside)

Operation Inside project Outside project
rm, rm -rf Allowed Blocked
mv (source and destination) Allowed Blocked
cp (source and destination) Allowed Blocked
ln (source and target) Allowed Blocked
chmod / chown Allowed Blocked
> / >> redirect Allowed Blocked
tee / tee -a Allowed Blocked
curl -o / curl --output Allowed Blocked
wget -O / wget --output-document Allowed Blocked
find -delete / find -exec rm Allowed Blocked
dd of= Allowed Blocked
install (source and destination) Allowed Blocked
rsync (source and destination) Allowed Blocked
tar -C / --directory= Allowed Blocked
unzip -d / cpio -D Allowed Blocked
Edit tool (file edits) Allowed Blocked
MultiEdit tool (multi-file edits) Allowed Blocked
Write tool (file creation) Allowed Blocked

Always blocked (unsafe to inspect)

Command Reason
bash -c "..." / sh -c "..." Nested shell — cannot inspect inner command
eval '...' Cannot safely parse evaluated code
Piping to sh / bash Inner commands invisible to guard
xargs rm/mv/cp/... Arguments cannot be validated
$(...) / backticks (outside single quotes) Command substitution target is uninspectable. Single-quoted forms like '$(cmd)' and arithmetic expansion $((2+2)) are allowed.

Additional protections

  • Chained commands — splits on ;, &&, ||, | and checks each sub-command independently
  • cwd awareness — uses cwd from the hook event, so commands run outside the project (without an explicit cd) are also guarded
  • cd trackingcd /tmp && rm -rf something is blocked because cd left the project; cd $PROJECT && rm file is allowed even if the event cwd was outside
  • Destructive subcommands outside project — when running outside the project (via event cwd or cd), these are blocked: git clean -f, git checkout ., git restore ., git reset --hard, git push --force, git stash drop/clear, git branch -D, git reflog expire, rails db:drop/reset, rake db:drop/reset. Safe commands like git status, git log, rails routes remain allowed.
  • sudo prefix — stripped before checking, so sudo rm /etc/passwd is still blocked
  • find options — handles -L, -H, -P before the search path
  • Path traversal.. segments are resolved before boundary check
  • ~ and $HOME expansionrm ~/file and rm $HOME/file are correctly detected as outside-project
  • Symlink resolution — handles macOS /var/private/var, dereferences symlink chains in Edit/Write/MultiEdit (fail-closed after 20 hops)
  • /dev/null bit-bucketcurl -o /dev/null, 2>/dev/null, tee /dev/null, dd of=/dev/null, and all redirect target forms are allowed so routine probe and silencing workflows don't hit the boundary. Narrow exemption: the discard-only walkers short-circuit before is_write_permitted; sed -i /dev/null, truncate /dev/null, and cp|mv|ln ... /dev/null remain blocked because each performs a real filesystem write under /dev/.

Path allowlist (hooks/allowlist.conf)

Some paths legitimately live outside every project — e.g. Claude Code's auto-memory under ~/.claude/projects/<slug>/memory/, which needs to persist across projects by design. The allowlist file lets you permit writes to those paths without loosening the project boundary for everything else.

Format: one glob pattern per line; # starts a comment; ~ expands to $HOME; ** matches across path segments (bash globstar), * within a single segment.

Defaults shipped with the plugin:

  • ~/.claude/projects/*/memory/** — Claude Code auto-memory

Warning

Do not mass-add entries to the allowlist. Every entry is an escape hatch from the boundary, and Claude is creative enough to find non-obvious workarounds through allowed paths — for example: symlink-chasing from an allowlisted dir into sensitive files, writing executable content that some other tool later sources, or staging payloads in an allowed dir before moving them elsewhere. Widening the allowlist to something like ~/.claude/** would let Claude overwrite settings.json or your shell rc files. Keep entries narrow, purpose-specific, and comment each one with the reason it exists. Prefer asking Claude for explicit per-write permission over adding entries.

Known limitations

  • Paths with spaces work when properly quoted (single or double quotes). Unquoted paths with spaces are not supported.
  • Heredoc body contents are not inspected (only the first line of the command, where redirects are handled normally)
  • Brace expansion ({a,b,c}) is not enumerated — literal match only
  • ~user/ (home of another user) is not expanded; only ~/ (current user) is handled

Multiline git commits ($(cat <<EOF) is blocked)

The common idiom git commit -m "$(cat <<'EOF' … EOF)" is blocked on purpose — command substitution $(…) is fail-closed because the inner command is not inspectable in the general case ($(cat && rm /etc/passwd) looks identical to the parser). Making an exception for one shape of cat would just open a new bypass category.

Equivalent patterns that pass the guard:

# A) Heredoc piped on stdin — no $(), just a redirect
git commit -F - <<'EOF'
Subject line

Body paragraph.
EOF

# B) Write the message to a file, commit from file
# (this is what Claude Code falls back to automatically)
git commit -F .git/COMMIT_EDITMSG_DRAFT

# C) Repeated -m: each flag becomes one paragraph
git commit -m "Subject line" -m "Body paragraph."

When Claude Code hits the block it will transparently switch to pattern B — cost is one extra tool call (Write + Bash instead of single Bash). No user action needed: the plugin registers a SessionStart hook that injects a one-line hint into every session, so Claude picks the right workaround on the first try.

Install

Direct:

claude --plugin-dir /path/to/claude-code-project-boundary

From marketplace:

/plugin marketplace add davepoon/buildwithclaude
/plugin install project-boundary@buildwithclaude

How it works

Pure-bash PreToolUse hooks for Bash, Edit, MultiEdit, and Write tools. The Bash hook splits chained commands and resolves target paths (handling symlinks, .., ~, $HOME); the Edit, MultiEdit, and Write hooks perform file path boundary checks against $CLAUDE_PROJECT_DIR. Dependencies: bash + jq.

Testing

bash tests/test_guard.sh

Full test suite covering all guard scenarios. CI runs on Ubuntu and macOS.

License

MIT

About

Claude Code plugin: scope-aware protection — allows destructive ops (rm, mv, chmod) within project, blocks them outside. Built for dangerouslySkipPermissions mode.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages