Sandbox
@justi/claude-code-project-boundary

Claude Code plugin for project-boundary command safety

This plugin sits in Claude Code’s PreToolUse hooks and checks Bash, Edit, MultiEdit, and Write actions before they run. It allows writes and destructive operations inside the project, but blocks them when paths escape the project directory, using `$CLAUDE_PROJECT_DIR` and path resolution rules.

86 stars3 forksShellUpdated 1mo ago
Who it's for

Builders who use Claude Code and want destructive commands checked against the current project.

What it delivers

You can let Claude refactor and edit files normally without letting it delete or rewrite files outside the repo.

What it does

Boundary-checked destructive commands

Allows `rm`, `mv`, `cp`, `chmod`, `chown`, redirects, `tee`, `curl -o`, `wget -O`, and similar actions only when targets stay inside the project.

Shell command inspection

Splits chained Bash commands and blocks nested shell forms like `bash -c`, `eval`, `$(...)`, pipes to shell, and inline interpreter escapes.

File tool protection

Checks `Edit`, `MultiEdit`, and `Write` tool actions against the project boundary before they touch files.

Allowlist for safe external writes

Supports a narrow write-only allowlist for paths like Claude Code memory files without opening up destructive commands there.

Windows and symlink handling

Normalizes Windows paths, follows symlinks and reparse points, and resolves `~`, `$HOME`, and `..` before checking boundaries.

Test suite and smoke coverage

Includes shell tests for allowlists, bypass reproducers, true negatives, and Windows smoke checks.

How to get it

  1. 1Direct
    claude --plugin-dir /path/to/claude-code-project-boundary
  2. 2From marketplace
    /plugin marketplace add davepoon/buildwithclaude
    /plugin install project-boundary@buildwithclaude

README

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)

OperationInside projectOutside project
rm, rm -rfAllowedBlocked
mv (source and destination)AllowedBlocked
cp (source and destination)AllowedBlocked
ln (source and target)AllowedBlocked
chmod / chownAllowedBlocked
> / >> redirectAllowedBlocked
tee / tee -aAllowedBlocked
curl -o / curl --outputAllowedBlocked
wget -O / wget --output-documentAllowedBlocked
find -delete / find -exec rmAllowedBlocked
dd of=AllowedBlocked
install (source, destination, --target-directory=)AllowedBlocked
rsync (source, destination, --log-file=, --partial-dir=, --backup-dir=, --temp-dir=, --write-batch=, --only-write-batch=)AllowedBlocked
tar -C / --directory=AllowedBlocked
unzip -d / cpio -DAllowedBlocked
7z -o<dir> / 7z -w<dir> (extract verbs only)AllowedBlocked
Edit tool (file edits)AllowedBlocked
MultiEdit tool (multi-file edits)AllowedBlocked
Write tool (file creation)AllowedBlocked

Always blocked (unsafe to inspect)

CommandReason
bash -c "..." / sh -c "..."Nested shell — cannot inspect inner command
eval '...'Cannot safely parse evaluated code
Piping to sh / bashInner commands invisible to guard
xargs rm/mv/cp/...Arguments cannot be validated
python -c / ruby -e / perl -e / node --eval / php -r/-R/--run / Rscript -e / osascript -eInline interpreter code is opaque to the Bash parser
awk '... system("...") ...' (and similar | "sh")Awk programs can shell out without the guard seeing the inner command
env -S / env --split-string / env -C / env --chdirThese either smuggle a real command inside a string or change the working directory before the inner tool runs
$(...) / backticks (outside single quotes)Command substitution target is uninspectable. Single-quoted forms like '$(cmd)' and arithmetic expansion $((2+2)) are allowed.
$VAR / ${VAR} and positional / special parameters ($1$9, $@, $*, $#, $?, $$, $!, $-) outside single quotesVariable expansion target is uninspectable for the same reason as $(...). Only $HOME / ${HOME} is allowed (canonical home path). Use literal values inline, or reach for the Read / Grep tools instead of piping shell vars. ANSI-C $'…', i18n $"…", backslash-escaped \$VAR, single-quoted '$VAR', and quoted-heredoc bodies are unaffected.

Additional protections

  • Chained commands — splits on ;, &&, ||, |, and unquoted newlines, then 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 ~/your-repo && rm file is allowed even if the event cwd was outside ($PROJECT and other non-$HOME variables are uninspectable, so use ~/$HOME or a literal path)
  • 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/.
  • POSIX -- end-of-optionsinstall, rsync, sed -i, and truncate continue parsing operands after a literal --, so rsync … -- -outside/file and similar dash-prefixed targets are validated rather than silently skipped as flags.
  • Windows-native path tokens in COMMANDtee C:\Windows\System32\…, rm C:/Users/x/.ssh/id_rsa, redirects to drive-letter paths and UNC \\server\share\… are rewritten per-token via cygpath -u (MSYS2) before walkers run, then the boundary check rejects them. On non-MSYS2 shells Windows-shape tokens fail closed because they don't match the POSIX absolute-path pattern.
  • jq behaviour canary — the hook entry challenges jq with a randomised key/value JSON object on every invocation. A hostile shim that returns canned output (jq() { echo ""; }) cannot reproduce a per-call random value and the hook blocks, so the parser used to extract tool_input is provably real jq.
  • NTFS reparse-point traversal — junctions (mklink /J) and symbolic links (mklink /D) inside the project are followed to their physical target by cd -P (MSYS2 implements it via Win32 SetCurrentDirectory), so project/escape -> C:\Windows resolves to /c/Windows and writes through it are blocked. Regression-anchored on the Windows-smoke job.

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.

Scope: the allowlist is a WRITE exception only. It applies to the gentle write paths — Edit / Write / MultiEdit, redirects (> / >>), tee, curl -o, wget -O, dd of=, and similar — and to cd into an allowlisted directory. It deliberately does not apply to destructive or move/copy operations (rm, mv, cp, ln, chmod, chown, find -delete, find -exec rm, install, rsync, tar -C, unzip -d, cpio -D) or to script execution and shell redirection from outside paths. An allowlist entry that grants WRITE to ~/.claude/projects/*/memory/** will not let rm or rsync run against that path.

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.
  • 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.

The supported pattern is heredoc on stdin:

git commit -F - <<'EOF'
Subject line

Body paragraph.
EOF

Repeated -m works for shorter messages where every paragraph fits on one line:

git commit -m "Subject line" -m "Body paragraph."

Avoid: writing the message to .git/COMMIT_* files (triggers a Write-tool prompt) or to /tmp/*_msg.txt (outside-project, blocked). The SessionStart hook ships a one-line hint that points Claude at the heredoc form on the first try, so no manual nudging is needed.

Requirements

bash and jq must be on the PATH of the hook shell. macOS and most Linux distros ship bash; jq is usually present but install it explicitly if missing — without jq the hook fails closed with a clear BLOCKED: 'jq' is required ... message.

PlatformInstall
macOSbrew install jq
Debian/Ubuntuapt install jq
Fedora/RHELdnf install jq
Archpacman -S jq
Windows (MSYS2)pacman -S jq
Windows (Scoop/Winget)scoop install jq or winget install jqlang.jq

On Windows the plugin runs under MSYS2 bash; cygpath (shipped with MSYS2) is required for Windows-native paths (C:\…, \\server\…) to be normalized — without it those paths fail closed.

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, macOS, and Windows (MSYS2 smoke job — minimal end-to-end coverage of Windows-native path handling and the NTFS reparse-point regression anchor).

License

MIT

Files in the repo

Repository payload9 top-level entries
  • .claude-plugin
  • .github
  • hooks
  • tests
  • .gitignore
  • CHANGELOG.md
  • CLAUDE.md
  • LICENSE
  • README.md

Discussion (0)

Ask about usage, or say what you built with it

Sign in to join the discussion.

No comments yet. Be the first to say what this is good for.

More plugins

Makes your AI agent think like the laziest senior dev in the room. The best code is the code you never wrote.

138k
1 add

Graphs that teach > graphs that impress. Turn any code into an interactive knowledge graph you can explore, search, and ask questions about. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, and more.

82k
code-yeongyu/
oh-my-openagent

OmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.

69k

Persistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode + More

94k

Opinionated Oxlint rules for rejecting low-evidence TypeScript and JavaScript patterns

4.3k