codeforge-dev 1.13.0 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/CHANGELOG.md +140 -4
- package/.devcontainer/CLAUDE.md +61 -276
- package/.devcontainer/README.md +1 -1
- package/.devcontainer/config/defaults/ccstatusline-settings.json +147 -0
- package/.devcontainer/config/defaults/main-system-prompt.md +6 -1
- package/.devcontainer/config/defaults/rules/spec-workflow.md +1 -55
- package/.devcontainer/config/file-manifest.json +14 -0
- package/.devcontainer/devcontainer.json +19 -1
- package/.devcontainer/docs/optional-features.md +0 -65
- package/.devcontainer/docs/plugins.md +38 -23
- package/.devcontainer/features/ast-grep/devcontainer-feature.json +0 -1
- package/.devcontainer/features/biome/install.sh +13 -0
- package/.devcontainer/features/ccburn/devcontainer-feature.json +0 -1
- package/.devcontainer/features/ccms/devcontainer-feature.json +0 -1
- package/.devcontainer/features/ccms/install.sh +1 -1
- package/.devcontainer/features/ccstatusline/devcontainer-feature.json +0 -1
- package/.devcontainer/features/ccstatusline/install.sh +17 -115
- package/.devcontainer/features/ccusage/devcontainer-feature.json +0 -1
- package/.devcontainer/features/chromaterm/README.md +42 -0
- package/.devcontainer/features/chromaterm/chromaterm.yml +35 -0
- package/.devcontainer/features/chromaterm/devcontainer-feature.json +22 -0
- package/.devcontainer/features/chromaterm/install.sh +113 -0
- package/.devcontainer/features/claude-monitor/devcontainer-feature.json +0 -1
- package/.devcontainer/features/claude-session-dashboard/README.md +2 -2
- package/.devcontainer/features/claude-session-dashboard/devcontainer-feature.json +1 -2
- package/.devcontainer/features/claude-session-dashboard/install.sh +2 -2
- package/.devcontainer/features/kitty-terminfo/README.md +32 -0
- package/.devcontainer/features/kitty-terminfo/devcontainer-feature.json +13 -0
- package/.devcontainer/features/kitty-terminfo/install.sh +72 -0
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +0 -1
- package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +0 -1
- package/.devcontainer/features/shellcheck/install.sh +6 -2
- package/.devcontainer/features/tree-sitter/devcontainer-feature.json +0 -1
- package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +37 -69
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/.claude-plugin/plugin.json +0 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/README.md +197 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/architect.md +3 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/bash-exec.md +3 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/claude-guide.md +4 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/debug-logs.md +6 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/dependency-analyst.md +5 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/doc-writer.md +4 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/explorer.md +3 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/generalist.md +9 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/git-archaeologist.md +3 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/migrator.md +4 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/perf-profiler.md +4 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/refactorer.md +5 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/researcher.md +5 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/security-auditor.md +4 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/spec-writer.md +3 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/statusline-config.md +4 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/test-writer.md +4 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/hooks/hooks.json +23 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/guard-readonly-bash.py +2 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/inject-cwd.py +7 -4
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/task-completed-check.py +166 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/teammate-idle-check.py +81 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/verify-no-regression.py +14 -10
- package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/verify-tests-pass.py +2 -14
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/README.md +17 -31
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/hooks/hooks.json +5 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/advisory-test-runner.py +9 -8
- package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/README.md +28 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/README.md +28 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +2 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/README.md +28 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +0 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/README.md +28 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected-bash.py +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +2 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/session-context/.claude-plugin/plugin.json +0 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/session-context/README.md +140 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/session-context/hooks/hooks.json +0 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/session-context/scripts/commit-reminder.py +3 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/session-context/scripts/git-state-injector.py +18 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/session-context/scripts/todo-harvester.py +9 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/.claude-plugin/plugin.json +0 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/README.md +158 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/hooks/hooks.json +1 -14
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py +189 -100
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/api-design/SKILL.md +9 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/ast-grep-patterns/SKILL.md +7 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/claude-agent-sdk/SKILL.md +8 -8
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/claude-code-headless/SKILL.md +8 -9
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/debugging/SKILL.md +11 -7
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/dependency-management/SKILL.md +10 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/docker/SKILL.md +8 -8
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/docker-py/SKILL.md +9 -7
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/documentation-patterns/SKILL.md +7 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/fastapi/SKILL.md +9 -8
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/git-forensics/SKILL.md +11 -9
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/migration-patterns/SKILL.md +7 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/performance-profiling/SKILL.md +10 -8
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/pydantic-ai/SKILL.md +8 -7
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/refactoring-patterns/SKILL.md +9 -8
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/security-checklist/SKILL.md +9 -8
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/skill-building/SKILL.md +7 -7
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/sqlite/SKILL.md +9 -7
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/svelte5/SKILL.md +7 -8
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/team/SKILL.md +71 -5
- package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/testing/SKILL.md +10 -7
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/.claude-plugin/plugin.json +0 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/README.md +192 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/hooks/hooks.json +0 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/scripts/spec-reminder.py +3 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-build/SKILL.md +9 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-check/SKILL.md +10 -5
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-init/SKILL.md +8 -4
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-new/SKILL.md +8 -4
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-refine/SKILL.md +10 -7
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-review/SKILL.md +10 -6
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-update/SKILL.md +10 -5
- package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/specification-writing/SKILL.md +9 -9
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +1 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/README.md +28 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/hooks/hooks.json +0 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/scripts/ticket-linker.py +9 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/README.md +104 -32
- package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/hooks/hooks.json +49 -3
- package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/guard-workspace-scope.py +269 -56
- package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/inject-workspace-cwd.py +44 -0
- package/.devcontainer/scripts/setup-aliases.sh +13 -5
- package/.devcontainer/scripts/setup-config.sh +1 -0
- package/README.md +5 -5
- package/package.json +6 -2
- package/setup.js +3 -2
- package/.devcontainer/.env +0 -33
- package/.devcontainer/features/README.md +0 -126
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +0 -7
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/README.md +0 -81
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +0 -17
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/__pycache__/format-on-stop.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-on-stop.py +0 -297
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +0 -7
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/README.md +0 -92
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +0 -17
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/__pycache__/lint-file.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +0 -536
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/__pycache__/block-dangerous.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/__pycache__/guard-protected.cpython-314.pyc +0 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +0 -184
- package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/__pycache__/guard-workspace-scope.cpython-314.pyc +0 -0
- /package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/{.claude-plugin/commands/debug.md → skills/debug/SKILL.md} +0 -0
- /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272create-pr.md" → skills/ticketcreate-pr/SKILL.md} +0 -0
- /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272new.md" → skills/ticketnew/SKILL.md} +0 -0
- /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272review-commit.md" → skills/ticketreview-commit/SKILL.md} +0 -0
- /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272work.md" → skills/ticketwork/SKILL.md} +0 -0
|
@@ -1,33 +1,49 @@
|
|
|
1
1
|
# workspace-scope-guard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Nuclear workspace scope enforcement for Claude Code. Blocks ALL operations (read, write, bash) outside the current working directory. Permanently blacklists `/workspaces/.devcontainer/` — no exceptions, no bypass, even from workspace root.
|
|
4
4
|
|
|
5
5
|
## What It Does
|
|
6
6
|
|
|
7
|
-
Intercepts file
|
|
7
|
+
Intercepts all file and bash operations and enforces strict scope boundaries:
|
|
8
8
|
|
|
9
9
|
| Operation | Out-of-scope behavior |
|
|
10
10
|
|-----------|-----------------------|
|
|
11
|
-
| Write, Edit, NotebookEdit | **Blocked** (exit 2)
|
|
12
|
-
| Read, Glob, Grep | **
|
|
11
|
+
| Write, Edit, NotebookEdit | **Blocked** (exit 2) |
|
|
12
|
+
| Read, Glob, Grep | **Blocked** (exit 2) |
|
|
13
|
+
| Bash | **Blocked** (exit 2) — two-layer detection |
|
|
14
|
+
| Unknown tools | **Blocked** (exit 2) |
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
### Blacklisted Paths
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
`/workspaces/.devcontainer/` is permanently blocked for ALL operations — reads, writes, and bash commands. This blacklist:
|
|
19
|
+
|
|
20
|
+
- Runs BEFORE all other checks (scope, allowlist, cwd bypass)
|
|
21
|
+
- Cannot be overridden, even when cwd is `/workspaces`
|
|
22
|
+
- Prevents the most common scope escape: writing to the workspace-root devcontainer instead of the project's
|
|
23
|
+
|
|
24
|
+
### Allowlisted Paths
|
|
17
25
|
|
|
18
26
|
These paths are always permitted regardless of working directory:
|
|
19
27
|
|
|
20
28
|
| Path | Reason |
|
|
21
29
|
|------|--------|
|
|
22
|
-
| `/workspaces/.claude/` | Claude
|
|
23
|
-
| `/workspaces/.tmp/` | Temporary files |
|
|
24
|
-
| `/workspaces/.devcontainer/` | Container configuration |
|
|
30
|
+
| `/workspaces/.claude/` | Claude config, plans, rules |
|
|
25
31
|
| `/tmp/` | System temp directory |
|
|
26
|
-
|
|
32
|
+
|
|
33
|
+
### CWD Context Injection
|
|
34
|
+
|
|
35
|
+
The plugin injects working directory awareness on four hook events:
|
|
36
|
+
|
|
37
|
+
| Hook Event | Purpose |
|
|
38
|
+
|-----------|---------|
|
|
39
|
+
| SessionStart | Set scope context at session begin |
|
|
40
|
+
| UserPromptSubmit | Remind scope on every prompt |
|
|
41
|
+
| PreToolUse | Context alongside scope enforcement |
|
|
42
|
+
| SubagentStart | Ensure subagents know their scope |
|
|
27
43
|
|
|
28
44
|
## How It Works
|
|
29
45
|
|
|
30
|
-
### Hook Lifecycle
|
|
46
|
+
### Hook Lifecycle (File Tools)
|
|
31
47
|
|
|
32
48
|
```
|
|
33
49
|
Claude calls Read, Write, Edit, NotebookEdit, Glob, or Grep
|
|
@@ -36,24 +52,43 @@ Claude calls Read, Write, Edit, NotebookEdit, Glob, or Grep
|
|
|
36
52
|
│
|
|
37
53
|
└─→ guard-workspace-scope.py
|
|
38
54
|
│
|
|
39
|
-
├─→
|
|
40
|
-
├─→
|
|
41
|
-
├─→
|
|
42
|
-
├─→
|
|
43
|
-
├─→ Path
|
|
44
|
-
├─→
|
|
45
|
-
└─→
|
|
55
|
+
├─→ Extract target path from tool input
|
|
56
|
+
├─→ Resolve via os.path.realpath() (handles symlinks)
|
|
57
|
+
├─→ BLACKLIST check (first!) → exit 2 if blacklisted
|
|
58
|
+
├─→ cwd is /workspaces? → allow (bypass, blacklist already checked)
|
|
59
|
+
├─→ Path within cwd? → allow
|
|
60
|
+
├─→ Path on allowlist? → allow
|
|
61
|
+
└─→ Out of scope → exit 2 (block)
|
|
46
62
|
```
|
|
47
63
|
|
|
48
|
-
###
|
|
64
|
+
### Hook Lifecycle (Bash)
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Claude calls Bash
|
|
68
|
+
│
|
|
69
|
+
└─→ PreToolUse hook fires
|
|
70
|
+
│
|
|
71
|
+
└─→ guard-workspace-scope.py
|
|
72
|
+
│
|
|
73
|
+
├─→ Extract write targets (Layer 1) + workspace paths (Layer 2)
|
|
74
|
+
├─→ BLACKLIST check on ALL extracted paths → exit 2 if any blacklisted
|
|
75
|
+
├─→ cwd is /workspaces? → allow (bypass, blacklist already checked)
|
|
76
|
+
├─→ Layer 1: Check write targets against scope
|
|
77
|
+
│ ├─→ System command exemption (only if ALL targets are system paths)
|
|
78
|
+
│ └─→ exit 2 if any write target out of scope
|
|
79
|
+
└─→ Layer 2: Scan ALL /workspaces/ paths in command (ALWAYS runs)
|
|
80
|
+
└─→ exit 2 if any workspace path out of scope
|
|
81
|
+
```
|
|
49
82
|
|
|
50
|
-
|
|
51
|
-
- Symbolic links that point outside the working directory
|
|
52
|
-
- Git worktree paths (`.git` file containing `gitdir:`)
|
|
83
|
+
### Bash Two-Layer Detection
|
|
53
84
|
|
|
54
|
-
|
|
85
|
+
**Layer 1 — Write target extraction:** 20+ regex patterns extract file paths from write operations (redirects, cp, mv, touch, mkdir, rm, ln, rsync, chmod, chown, dd, wget, curl, tar, unzip, gcc, sqlite3, etc.). Each target is resolved and scope-checked.
|
|
55
86
|
|
|
56
|
-
|
|
87
|
+
System commands (git, pip, npm, etc.) get a Layer 1 exemption ONLY when ALL write targets resolve to system paths (`/usr/`, `/bin/`, `/sbin/`, `/lib/`, `/opt/`, `/proc/`, `/sys/`, `/dev/`, `/var/`, `/etc/`). Any `/workspaces/` write target outside cwd cancels the exemption.
|
|
88
|
+
|
|
89
|
+
**Layer 2 — Workspace path scan (ALWAYS runs):** Regex scans the entire command for any `/workspaces/` path string. Catches paths in inline scripts (`python3 -c`), variable assignments, quoted strings, and anything Layer 1 misses. No exemptions, no bypass.
|
|
90
|
+
|
|
91
|
+
### Path Field Mapping
|
|
57
92
|
|
|
58
93
|
| Tool | Input Field |
|
|
59
94
|
|------|-------------|
|
|
@@ -63,29 +98,66 @@ The script extracts the target path from different tool input fields:
|
|
|
63
98
|
| NotebookEdit | `notebook_path` |
|
|
64
99
|
| Glob | `path` |
|
|
65
100
|
| Grep | `path` |
|
|
101
|
+
| Bash | `command` (multi-path extraction) |
|
|
66
102
|
|
|
67
103
|
### Error Handling
|
|
68
104
|
|
|
69
105
|
| Scenario | Behavior |
|
|
70
106
|
|----------|----------|
|
|
71
|
-
| JSON parse failure |
|
|
72
|
-
|
|
|
107
|
+
| JSON parse failure | **Blocked** (exit 2) — fail closed |
|
|
108
|
+
| Any exception | **Blocked** (exit 2) — fail closed |
|
|
109
|
+
| Hook timeout | Fails open (Claude Code runtime limitation) — mitigated by 10s timeout and pure computation (no I/O) |
|
|
110
|
+
|
|
111
|
+
## Known Limitations
|
|
112
|
+
|
|
113
|
+
| Limitation | Why It's Accepted |
|
|
114
|
+
|-----------|-------------------|
|
|
115
|
+
| Pre-set env vars (`$OUTDIR/file` from prior command) | Layer 2 only catches literal `/workspaces/` strings. Variable set in same command IS caught. |
|
|
116
|
+
| Base64-encoded paths | Not an accidental misuse pattern |
|
|
117
|
+
| Bash brace expansion | Not an accidental directory construction pattern |
|
|
118
|
+
| Variable concatenation across statements | No single literal path exists to match |
|
|
119
|
+
| Hook timeout fails open | Mitigated: 10s timeout, pure computation, no I/O |
|
|
120
|
+
|
|
121
|
+
## Installation
|
|
122
|
+
|
|
123
|
+
### CodeForge DevContainer
|
|
124
|
+
|
|
125
|
+
Pre-installed and activated automatically — no setup needed.
|
|
126
|
+
|
|
127
|
+
### From GitHub
|
|
128
|
+
|
|
129
|
+
Use this plugin in any Claude Code setup:
|
|
130
|
+
|
|
131
|
+
1. Clone the [CodeForge](https://github.com/AnExiledDev/CodeForge) repository:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
git clone https://github.com/AnExiledDev/CodeForge.git
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
2. Enable the plugin in your `.claude/settings.json`:
|
|
73
138
|
|
|
74
|
-
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"enabledPlugins": {
|
|
142
|
+
"workspace-scope-guard@<clone-path>/.devcontainer/plugins/devs-marketplace": true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
75
146
|
|
|
76
|
-
|
|
147
|
+
Replace `<clone-path>` with the absolute path to your CodeForge clone.
|
|
77
148
|
|
|
78
149
|
## Plugin Structure
|
|
79
150
|
|
|
80
151
|
```
|
|
81
152
|
workspace-scope-guard/
|
|
82
153
|
├── .claude-plugin/
|
|
83
|
-
│ └── plugin.json
|
|
154
|
+
│ └── plugin.json # Plugin metadata
|
|
84
155
|
├── hooks/
|
|
85
|
-
│ └── hooks.json
|
|
156
|
+
│ └── hooks.json # Hook registrations (6 events)
|
|
86
157
|
├── scripts/
|
|
87
|
-
│
|
|
88
|
-
└──
|
|
158
|
+
│ ├── guard-workspace-scope.py # Scope enforcement (PreToolUse)
|
|
159
|
+
│ └── inject-workspace-cwd.py # CWD context injection (4 events)
|
|
160
|
+
└── README.md # This file
|
|
89
161
|
```
|
|
90
162
|
|
|
91
163
|
## Requirements
|
package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/hooks/hooks.json
CHANGED
|
@@ -1,14 +1,60 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "
|
|
2
|
+
"description": "Nuclear workspace scope enforcement — blocks all operations outside cwd, permanently blacklists /workspaces/.devcontainer/",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
6
|
-
"matcher": "Read|Write|Edit|NotebookEdit|Glob|Grep",
|
|
6
|
+
"matcher": "Read|Write|Edit|NotebookEdit|Glob|Grep|Bash",
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
10
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/guard-workspace-scope.py",
|
|
11
|
-
"timeout":
|
|
11
|
+
"timeout": 10
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"matcher": "",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/inject-workspace-cwd.py",
|
|
21
|
+
"timeout": 3
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"SessionStart": [
|
|
27
|
+
{
|
|
28
|
+
"matcher": "",
|
|
29
|
+
"hooks": [
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/inject-workspace-cwd.py",
|
|
33
|
+
"timeout": 3
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"UserPromptSubmit": [
|
|
39
|
+
{
|
|
40
|
+
"matcher": "",
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/inject-workspace-cwd.py",
|
|
45
|
+
"timeout": 3
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"SubagentStart": [
|
|
51
|
+
{
|
|
52
|
+
"matcher": "",
|
|
53
|
+
"hooks": [
|
|
54
|
+
{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/inject-workspace-cwd.py",
|
|
57
|
+
"timeout": 3
|
|
12
58
|
}
|
|
13
59
|
]
|
|
14
60
|
}
|
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
Nuclear workspace scope enforcement.
|
|
4
4
|
|
|
5
|
-
Blocks
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Blocks ALL operations (read, write, bash) outside the current working directory.
|
|
6
|
+
Permanently blacklists /workspaces/.devcontainer/ — no exceptions, no bypass.
|
|
7
|
+
Bash enforcement via two-layer detection: write target extraction + workspace path scan.
|
|
8
|
+
Fails closed on any error.
|
|
9
9
|
|
|
10
10
|
Exit code 2 blocks the operation with an error message.
|
|
11
|
-
Exit code 0 allows the operation to proceed
|
|
11
|
+
Exit code 0 allows the operation to proceed.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
15
|
import os
|
|
16
|
+
import re
|
|
17
|
+
import shlex
|
|
16
18
|
import sys
|
|
17
19
|
|
|
18
|
-
#
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# BLACKLIST — checked FIRST, overrides everything.
|
|
22
|
+
# Nothing touches these paths. Ever. No exceptions.
|
|
23
|
+
# Checked before allowlist, before scope check, before cwd bypass.
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
BLACKLISTED_PREFIXES = [
|
|
26
|
+
"/workspaces/.devcontainer/",
|
|
27
|
+
"/workspaces/.devcontainer", # exact match (no trailing slash)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Paths always allowed regardless of working directory
|
|
19
31
|
ALLOWED_PREFIXES = [
|
|
20
|
-
"/workspaces/.
|
|
21
|
-
"/tmp/",
|
|
22
|
-
"/home/vscode/",
|
|
32
|
+
"/workspaces/.claude/", # Claude config, plans, rules
|
|
33
|
+
"/tmp/", # System scratch
|
|
23
34
|
]
|
|
24
35
|
|
|
25
36
|
WRITE_TOOLS = {"Write", "Edit", "NotebookEdit"}
|
|
26
37
|
READ_TOOLS = {"Read", "Glob", "Grep"}
|
|
38
|
+
ALL_FILE_TOOLS = WRITE_TOOLS | READ_TOOLS
|
|
27
39
|
|
|
28
40
|
# Tool input field that contains the target path
|
|
29
41
|
PATH_FIELDS = {
|
|
@@ -35,6 +47,76 @@ PATH_FIELDS = {
|
|
|
35
47
|
"Grep": "path",
|
|
36
48
|
}
|
|
37
49
|
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Bash Layer 1: Write target patterns
|
|
52
|
+
# Ported from guard-protected-bash.py + new patterns
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
WRITE_PATTERNS = [
|
|
55
|
+
# --- Ported from guard-protected-bash.py ---
|
|
56
|
+
r"(?:>|>>)\s*([^\s;&|]+)", # > file, >> file
|
|
57
|
+
r"\btee\s+(?:-a\s+)?([^\s;&|]+)", # tee file
|
|
58
|
+
r"\b(?:cp|mv)\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)", # cp/mv src dest
|
|
59
|
+
r'\bsed\s+-i[^\s]*\s+(?:\'[^\']*\'\s+|"[^"]*"\s+|[^\s]+\s+)*([^\s;&|]+)', # sed -i
|
|
60
|
+
r"\bcat\s+(?:<<[^\s]*\s+)?>\s*([^\s;&|]+)", # cat > file
|
|
61
|
+
# --- New patterns ---
|
|
62
|
+
r"\btouch\s+(?:-[^\s]+\s+)*([^\s;&|]+)", # touch file
|
|
63
|
+
r"\bmkdir\s+(?:-[^\s]+\s+)*([^\s;&|]+)", # mkdir [-p] dir
|
|
64
|
+
r"\brm\s+(?:-[^\s]+\s+)*([^\s;&|]+)", # rm [-rf] path
|
|
65
|
+
r"\bln\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)", # ln [-s] src dest
|
|
66
|
+
r"\binstall\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)", # install src dest
|
|
67
|
+
r"\brsync\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)", # rsync src dest
|
|
68
|
+
r"\bchmod\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)", # chmod mode path
|
|
69
|
+
r"\bchown\s+(?:-[^\s]+\s+)*[^\s:]+(?::[^\s]+)?\s+([^\s;&|]+)", # chown owner[:group] path
|
|
70
|
+
r"\bdd\b[^;|&]*\bof=([^\s;&|]+)", # dd of=path
|
|
71
|
+
r"\bwget\s+(?:-[^\s]+\s+)*-O\s+([^\s;&|]+)", # wget -O path
|
|
72
|
+
r"\bcurl\s+(?:-[^\s]+\s+)*-o\s+([^\s;&|]+)", # curl -o path
|
|
73
|
+
r"\btar\s+(?:-[^\s]+\s+)*-C\s+([^\s;&|]+)", # tar -C dir
|
|
74
|
+
r"\bunzip\s+(?:-[^\s]+\s+)*-d\s+([^\s;&|]+)", # unzip -d dir
|
|
75
|
+
r"\b(?:gcc|g\+\+|cc|c\+\+|clang)\s+(?:-[^\s]+\s+)*-o\s+([^\s;&|]+)", # gcc -o out
|
|
76
|
+
r"\bsqlite3\s+([^\s;&|]+)", # sqlite3 dbpath
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Bash Layer 2: Workspace path scan (ALWAYS runs, never exempt)
|
|
81
|
+
# Stops at: whitespace, ;, |, &, >, ), <, ', "
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
WORKSPACE_PATH_RE = re.compile(r'/workspaces/[^\s;|&>)<\'"]+')
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# System command exemption (Layer 1 only)
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
SYSTEM_COMMANDS = frozenset({
|
|
89
|
+
"git", "pip", "pip3", "npm", "npx", "yarn", "pnpm",
|
|
90
|
+
"apt-get", "apt", "cargo", "go", "docker", "make", "cmake",
|
|
91
|
+
"node", "python3", "python", "ruby", "gem", "bundle",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
SYSTEM_PATH_PREFIXES = (
|
|
95
|
+
"/usr/", "/bin/", "/sbin/", "/lib/", "/opt/",
|
|
96
|
+
"/proc/", "/sys/", "/dev/", "/var/", "/etc/",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Core check functions
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def is_blacklisted(resolved_path: str) -> bool:
|
|
105
|
+
"""Check if resolved_path is under a permanently blocked directory."""
|
|
106
|
+
return (resolved_path == "/workspaces/.devcontainer"
|
|
107
|
+
or resolved_path.startswith("/workspaces/.devcontainer/"))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_in_scope(resolved_path: str, cwd: str) -> bool:
|
|
111
|
+
"""Check if resolved_path is within the working directory."""
|
|
112
|
+
cwd_prefix = cwd if cwd.endswith("/") else cwd + "/"
|
|
113
|
+
return resolved_path == cwd or resolved_path.startswith(cwd_prefix)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def is_allowlisted(resolved_path: str) -> bool:
|
|
117
|
+
"""Check if resolved_path falls under an allowed prefix."""
|
|
118
|
+
return any(resolved_path.startswith(prefix) for prefix in ALLOWED_PREFIXES)
|
|
119
|
+
|
|
38
120
|
|
|
39
121
|
def get_target_path(tool_name: str, tool_input: dict) -> str | None:
|
|
40
122
|
"""Extract the target path from tool input.
|
|
@@ -48,16 +130,147 @@ def get_target_path(tool_name: str, tool_input: dict) -> str | None:
|
|
|
48
130
|
return tool_input.get(field) or None
|
|
49
131
|
|
|
50
132
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return resolved_path == cwd or resolved_path.startswith(cwd_prefix)
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# Bash enforcement
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
55
136
|
|
|
137
|
+
def extract_write_targets(command: str) -> list[str]:
|
|
138
|
+
"""Extract file paths that the command writes to (Layer 1)."""
|
|
139
|
+
targets = []
|
|
140
|
+
for pattern in WRITE_PATTERNS:
|
|
141
|
+
for match in re.finditer(pattern, command):
|
|
142
|
+
target = match.group(1).strip("'\"")
|
|
143
|
+
if target:
|
|
144
|
+
targets.append(target)
|
|
145
|
+
return targets
|
|
56
146
|
|
|
57
|
-
def is_allowlisted(resolved_path: str) -> bool:
|
|
58
|
-
"""Check if resolved_path falls under an allowed prefix."""
|
|
59
|
-
return any(resolved_path.startswith(prefix) for prefix in ALLOWED_PREFIXES)
|
|
60
147
|
|
|
148
|
+
def extract_primary_command(command: str) -> str:
|
|
149
|
+
"""Extract the primary command, stripping sudo/env/variable prefixes."""
|
|
150
|
+
try:
|
|
151
|
+
tokens = shlex.split(command)
|
|
152
|
+
except ValueError:
|
|
153
|
+
# Unclosed quotes or other parse errors — no exemption
|
|
154
|
+
return ""
|
|
155
|
+
i = 0
|
|
156
|
+
while i < len(tokens):
|
|
157
|
+
tok = tokens[i]
|
|
158
|
+
# Skip inline variable assignments: VAR=value
|
|
159
|
+
if "=" in tok and not tok.startswith("-") and tok.split("=")[0].isidentifier():
|
|
160
|
+
i += 1
|
|
161
|
+
continue
|
|
162
|
+
# Skip sudo and its flags
|
|
163
|
+
if tok == "sudo":
|
|
164
|
+
i += 1
|
|
165
|
+
while i < len(tokens) and tokens[i].startswith("-"):
|
|
166
|
+
flag = tokens[i]
|
|
167
|
+
i += 1
|
|
168
|
+
# Flags that consume the next token as an argument
|
|
169
|
+
if flag in ("-u", "-g", "-C", "-D", "-R", "-T"):
|
|
170
|
+
i += 1 # skip the argument too
|
|
171
|
+
continue
|
|
172
|
+
# Skip env and its variable assignments
|
|
173
|
+
if tok == "env":
|
|
174
|
+
i += 1
|
|
175
|
+
while i < len(tokens):
|
|
176
|
+
if "=" in tokens[i] and not tokens[i].startswith("-"):
|
|
177
|
+
i += 1 # skip VAR=val
|
|
178
|
+
elif tokens[i].startswith("-"):
|
|
179
|
+
i += 1 # skip env flags (-i, etc.)
|
|
180
|
+
else:
|
|
181
|
+
break
|
|
182
|
+
continue
|
|
183
|
+
# Skip nohup, nice, time
|
|
184
|
+
if tok in ("nohup", "nice", "time"):
|
|
185
|
+
i += 1
|
|
186
|
+
continue
|
|
187
|
+
return tok
|
|
188
|
+
return ""
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def check_bash_scope(command: str, cwd: str) -> None:
|
|
192
|
+
"""Enforce scope on Bash commands. Calls sys.exit(2) on violation."""
|
|
193
|
+
if not command:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# --- Extract paths from command ---
|
|
197
|
+
write_targets = extract_write_targets(command)
|
|
198
|
+
workspace_paths = WORKSPACE_PATH_RE.findall(command)
|
|
199
|
+
|
|
200
|
+
# --- BLACKLIST check (FIRST — before cwd bypass, before everything) ---
|
|
201
|
+
# Early exit on first blacklisted path found
|
|
202
|
+
for target in write_targets:
|
|
203
|
+
resolved = os.path.realpath(target.strip("'\""))
|
|
204
|
+
if is_blacklisted(resolved):
|
|
205
|
+
print(
|
|
206
|
+
f"Blocked: Bash command writes to blacklisted path '{target}'. "
|
|
207
|
+
f"/workspaces/.devcontainer/ is permanently blocked.",
|
|
208
|
+
file=sys.stderr,
|
|
209
|
+
)
|
|
210
|
+
sys.exit(2)
|
|
211
|
+
|
|
212
|
+
for path_str in workspace_paths:
|
|
213
|
+
resolved = os.path.realpath(path_str)
|
|
214
|
+
if is_blacklisted(resolved):
|
|
215
|
+
print(
|
|
216
|
+
f"Blocked: Bash command references blacklisted path '{path_str}'. "
|
|
217
|
+
f"/workspaces/.devcontainer/ is permanently blocked.",
|
|
218
|
+
file=sys.stderr,
|
|
219
|
+
)
|
|
220
|
+
sys.exit(2)
|
|
221
|
+
|
|
222
|
+
# --- cwd=/workspaces bypass (blacklist already checked above) ---
|
|
223
|
+
if cwd == "/workspaces":
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# --- Layer 1: Write target scope check ---
|
|
227
|
+
if write_targets:
|
|
228
|
+
primary_cmd = extract_primary_command(command)
|
|
229
|
+
is_system_cmd = primary_cmd in SYSTEM_COMMANDS
|
|
230
|
+
|
|
231
|
+
resolved_targets = [
|
|
232
|
+
(t, os.path.realpath(t.strip("'\""))) for t in write_targets
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
# System command exemption: skip Layer 1 ONLY if ALL targets are system paths
|
|
236
|
+
skip_layer1 = False
|
|
237
|
+
if is_system_cmd:
|
|
238
|
+
skip_layer1 = all(
|
|
239
|
+
any(r.startswith(sp) for sp in SYSTEM_PATH_PREFIXES)
|
|
240
|
+
for _, r in resolved_targets
|
|
241
|
+
)
|
|
242
|
+
# Override: if ANY target is under /workspaces/ outside cwd → NOT exempt
|
|
243
|
+
if skip_layer1:
|
|
244
|
+
for _, resolved in resolved_targets:
|
|
245
|
+
if resolved.startswith("/workspaces/") and not is_in_scope(resolved, cwd):
|
|
246
|
+
skip_layer1 = False
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
if not skip_layer1:
|
|
250
|
+
for target, resolved in resolved_targets:
|
|
251
|
+
if not is_in_scope(resolved, cwd) and not is_allowlisted(resolved):
|
|
252
|
+
print(
|
|
253
|
+
f"Blocked: Bash command writes to '{target}' which is "
|
|
254
|
+
f"outside the working directory ({cwd}).",
|
|
255
|
+
file=sys.stderr,
|
|
256
|
+
)
|
|
257
|
+
sys.exit(2)
|
|
258
|
+
|
|
259
|
+
# --- Layer 2: Workspace path scan (ALWAYS runs, never exempt) ---
|
|
260
|
+
for path_str in workspace_paths:
|
|
261
|
+
resolved = os.path.realpath(path_str)
|
|
262
|
+
if not is_in_scope(resolved, cwd) and not is_allowlisted(resolved):
|
|
263
|
+
print(
|
|
264
|
+
f"Blocked: Bash command references '{path_str}' which is "
|
|
265
|
+
f"outside the working directory ({cwd}).",
|
|
266
|
+
file=sys.stderr,
|
|
267
|
+
)
|
|
268
|
+
sys.exit(2)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# Main
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
61
274
|
|
|
62
275
|
def main():
|
|
63
276
|
try:
|
|
@@ -67,63 +280,63 @@ def main():
|
|
|
67
280
|
|
|
68
281
|
cwd = os.getcwd()
|
|
69
282
|
|
|
70
|
-
#
|
|
71
|
-
if
|
|
283
|
+
# --- Bash tool: separate code path ---
|
|
284
|
+
if tool_name == "Bash":
|
|
285
|
+
check_bash_scope(tool_input.get("command", ""), cwd)
|
|
72
286
|
sys.exit(0)
|
|
73
287
|
|
|
288
|
+
# --- File tools ---
|
|
74
289
|
target_path = get_target_path(tool_name, tool_input)
|
|
75
290
|
|
|
76
|
-
# No path
|
|
291
|
+
# No path → tool defaults to cwd, always in scope (for known file tools)
|
|
77
292
|
if target_path is None:
|
|
78
|
-
|
|
293
|
+
if tool_name in ALL_FILE_TOOLS:
|
|
294
|
+
sys.exit(0)
|
|
295
|
+
# Unknown tool with no recognizable path → block
|
|
296
|
+
print(
|
|
297
|
+
f"Blocked: Unknown tool '{tool_name}' — not in scope guard allowlist.",
|
|
298
|
+
file=sys.stderr,
|
|
299
|
+
)
|
|
300
|
+
sys.exit(2)
|
|
79
301
|
|
|
80
302
|
resolved = os.path.realpath(target_path)
|
|
81
303
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if is_allowlisted(resolved):
|
|
86
|
-
sys.exit(0)
|
|
87
|
-
|
|
88
|
-
# Out of scope
|
|
89
|
-
if tool_name in WRITE_TOOLS:
|
|
304
|
+
# BLACKLIST — checked FIRST, before cwd bypass
|
|
305
|
+
if is_blacklisted(resolved):
|
|
90
306
|
print(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
f"outside the working directory ({cwd}). Move to that "
|
|
96
|
-
f"project's directory first or work from /workspaces."
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
)
|
|
307
|
+
f"Blocked: {tool_name} targets '{target_path}' which is under "
|
|
308
|
+
f"blacklisted path /workspaces/.devcontainer/. This path is "
|
|
309
|
+
f"permanently blocked for all operations.",
|
|
310
|
+
file=sys.stderr,
|
|
100
311
|
)
|
|
101
312
|
sys.exit(2)
|
|
102
313
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
)
|
|
314
|
+
# cwd=/workspaces bypass (blacklist already checked)
|
|
315
|
+
if cwd == "/workspaces":
|
|
316
|
+
sys.exit(0)
|
|
317
|
+
|
|
318
|
+
# In-scope check
|
|
319
|
+
if is_in_scope(resolved, cwd):
|
|
320
|
+
sys.exit(0)
|
|
321
|
+
|
|
322
|
+
# Allowlist check
|
|
323
|
+
if is_allowlisted(resolved):
|
|
115
324
|
sys.exit(0)
|
|
116
325
|
|
|
117
|
-
#
|
|
118
|
-
|
|
326
|
+
# Out of scope — BLOCK for ALL tools
|
|
327
|
+
print(
|
|
328
|
+
f"Blocked: {tool_name} targets '{target_path}' which is outside "
|
|
329
|
+
f"the working directory ({cwd}). Move to that project's directory "
|
|
330
|
+
f"first or work from /workspaces.",
|
|
331
|
+
file=sys.stderr,
|
|
332
|
+
)
|
|
333
|
+
sys.exit(2)
|
|
119
334
|
|
|
120
335
|
except json.JSONDecodeError:
|
|
121
|
-
|
|
122
|
-
sys.exit(0)
|
|
336
|
+
sys.exit(2)
|
|
123
337
|
except Exception as e:
|
|
124
|
-
# Don't block on hook failure
|
|
125
338
|
print(f"Hook error: {e}", file=sys.stderr)
|
|
126
|
-
sys.exit(
|
|
339
|
+
sys.exit(2)
|
|
127
340
|
|
|
128
341
|
|
|
129
342
|
if __name__ == "__main__":
|