codeforge-dev 1.13.0 → 1.14.2

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.
Files changed (148) hide show
  1. package/.devcontainer/CHANGELOG.md +146 -4
  2. package/.devcontainer/CLAUDE.md +61 -276
  3. package/.devcontainer/README.md +1 -1
  4. package/.devcontainer/config/defaults/ccstatusline-settings.json +147 -0
  5. package/.devcontainer/config/defaults/main-system-prompt.md +6 -1
  6. package/.devcontainer/config/defaults/rules/spec-workflow.md +1 -55
  7. package/.devcontainer/config/file-manifest.json +14 -0
  8. package/.devcontainer/devcontainer.json +19 -1
  9. package/.devcontainer/docs/optional-features.md +0 -65
  10. package/.devcontainer/docs/plugins.md +38 -23
  11. package/.devcontainer/features/ast-grep/devcontainer-feature.json +0 -1
  12. package/.devcontainer/features/biome/install.sh +13 -0
  13. package/.devcontainer/features/ccburn/devcontainer-feature.json +0 -6
  14. package/.devcontainer/features/ccms/devcontainer-feature.json +0 -1
  15. package/.devcontainer/features/ccms/install.sh +1 -1
  16. package/.devcontainer/features/ccstatusline/devcontainer-feature.json +0 -1
  17. package/.devcontainer/features/ccstatusline/install.sh +17 -115
  18. package/.devcontainer/features/ccusage/devcontainer-feature.json +0 -6
  19. package/.devcontainer/features/chromaterm/README.md +42 -0
  20. package/.devcontainer/features/chromaterm/chromaterm.yml +35 -0
  21. package/.devcontainer/features/chromaterm/devcontainer-feature.json +22 -0
  22. package/.devcontainer/features/chromaterm/install.sh +113 -0
  23. package/.devcontainer/features/claude-monitor/devcontainer-feature.json +0 -6
  24. package/.devcontainer/features/claude-session-dashboard/README.md +2 -2
  25. package/.devcontainer/features/claude-session-dashboard/devcontainer-feature.json +2 -4
  26. package/.devcontainer/features/claude-session-dashboard/install.sh +2 -2
  27. package/.devcontainer/features/kitty-terminfo/README.md +32 -0
  28. package/.devcontainer/features/kitty-terminfo/devcontainer-feature.json +13 -0
  29. package/.devcontainer/features/kitty-terminfo/install.sh +72 -0
  30. package/.devcontainer/features/lsp-servers/devcontainer-feature.json +0 -1
  31. package/.devcontainer/features/mcp-qdrant/devcontainer-feature.json +0 -7
  32. package/.devcontainer/features/shellcheck/install.sh +6 -2
  33. package/.devcontainer/features/tree-sitter/devcontainer-feature.json +0 -7
  34. package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +37 -69
  35. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/.claude-plugin/plugin.json +0 -1
  36. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/README.md +197 -0
  37. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/architect.md +3 -1
  38. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/bash-exec.md +3 -0
  39. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/claude-guide.md +4 -1
  40. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/debug-logs.md +6 -1
  41. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/dependency-analyst.md +5 -1
  42. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/doc-writer.md +4 -1
  43. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/explorer.md +3 -1
  44. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/generalist.md +9 -1
  45. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/git-archaeologist.md +3 -0
  46. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/migrator.md +4 -1
  47. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/perf-profiler.md +4 -1
  48. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/refactorer.md +5 -1
  49. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/researcher.md +5 -1
  50. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/security-auditor.md +4 -1
  51. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/spec-writer.md +3 -1
  52. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/statusline-config.md +4 -1
  53. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/agents/test-writer.md +4 -1
  54. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/hooks/hooks.json +23 -1
  55. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/guard-readonly-bash.py +2 -2
  56. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/inject-cwd.py +7 -4
  57. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/task-completed-check.py +166 -0
  58. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/teammate-idle-check.py +81 -0
  59. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/verify-no-regression.py +14 -10
  60. package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/scripts/verify-tests-pass.py +2 -14
  61. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/README.md +17 -31
  62. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/hooks/hooks.json +5 -1
  63. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/advisory-test-runner.py +9 -8
  64. package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/README.md +28 -0
  65. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/README.md +28 -0
  66. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +2 -2
  67. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/README.md +28 -0
  68. package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/hooks/hooks.json +0 -1
  69. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/README.md +28 -0
  70. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected-bash.py +1 -1
  71. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +2 -2
  72. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/.claude-plugin/plugin.json +0 -1
  73. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/README.md +140 -0
  74. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/hooks/hooks.json +0 -1
  75. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/scripts/commit-reminder.py +3 -2
  76. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/scripts/git-state-injector.py +18 -2
  77. package/.devcontainer/plugins/devs-marketplace/plugins/session-context/scripts/todo-harvester.py +9 -1
  78. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/.claude-plugin/plugin.json +0 -1
  79. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/README.md +158 -0
  80. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/hooks/hooks.json +1 -14
  81. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py +189 -100
  82. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/api-design/SKILL.md +9 -6
  83. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/ast-grep-patterns/SKILL.md +7 -6
  84. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/claude-agent-sdk/SKILL.md +8 -8
  85. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/claude-code-headless/SKILL.md +8 -9
  86. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/debugging/SKILL.md +11 -7
  87. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/dependency-management/SKILL.md +10 -6
  88. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/docker/SKILL.md +8 -8
  89. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/docker-py/SKILL.md +9 -7
  90. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/documentation-patterns/SKILL.md +7 -6
  91. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/fastapi/SKILL.md +9 -8
  92. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/git-forensics/SKILL.md +11 -9
  93. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/migration-patterns/SKILL.md +7 -6
  94. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/performance-profiling/SKILL.md +10 -8
  95. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/pydantic-ai/SKILL.md +8 -7
  96. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/refactoring-patterns/SKILL.md +9 -8
  97. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/security-checklist/SKILL.md +9 -8
  98. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/skill-building/SKILL.md +7 -7
  99. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/sqlite/SKILL.md +9 -7
  100. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/svelte5/SKILL.md +7 -8
  101. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/team/SKILL.md +71 -5
  102. package/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/skills/testing/SKILL.md +10 -7
  103. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/.claude-plugin/plugin.json +0 -1
  104. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/README.md +192 -0
  105. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/hooks/hooks.json +0 -1
  106. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/scripts/spec-reminder.py +3 -2
  107. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-build/SKILL.md +9 -6
  108. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-check/SKILL.md +10 -5
  109. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-init/SKILL.md +8 -4
  110. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-new/SKILL.md +8 -4
  111. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-refine/SKILL.md +10 -7
  112. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-review/SKILL.md +10 -6
  113. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/spec-update/SKILL.md +10 -5
  114. package/.devcontainer/plugins/devs-marketplace/plugins/spec-workflow/skills/specification-writing/SKILL.md +9 -9
  115. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/plugin.json +1 -2
  116. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/README.md +28 -0
  117. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/hooks/hooks.json +0 -1
  118. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/scripts/ticket-linker.py +9 -1
  119. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/README.md +104 -32
  120. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/hooks/hooks.json +49 -3
  121. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/guard-workspace-scope.py +269 -56
  122. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/inject-workspace-cwd.py +44 -0
  123. package/.devcontainer/scripts/setup-aliases.sh +13 -5
  124. package/.devcontainer/scripts/setup-config.sh +1 -0
  125. package/README.md +5 -5
  126. package/package.json +6 -2
  127. package/setup.js +3 -2
  128. package/.devcontainer/.env +0 -33
  129. package/.devcontainer/features/README.md +0 -126
  130. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/.claude-plugin/plugin.json +0 -7
  131. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/README.md +0 -81
  132. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/hooks/hooks.json +0 -17
  133. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/__pycache__/format-on-stop.cpython-314.pyc +0 -0
  134. package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/scripts/format-on-stop.py +0 -297
  135. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/.claude-plugin/plugin.json +0 -7
  136. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/README.md +0 -92
  137. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/hooks/hooks.json +0 -17
  138. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/__pycache__/lint-file.cpython-314.pyc +0 -0
  139. package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/scripts/lint-file.py +0 -536
  140. package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/__pycache__/block-dangerous.cpython-314.pyc +0 -0
  141. package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/__pycache__/guard-protected.cpython-314.pyc +0 -0
  142. package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/.claude-plugin/system-prompt.md +0 -184
  143. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/__pycache__/guard-workspace-scope.cpython-314.pyc +0 -0
  144. /package/.devcontainer/plugins/devs-marketplace/plugins/agent-system/{.claude-plugin/commands/debug.md → skills/debug/SKILL.md} +0 -0
  145. /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272create-pr.md" → skills/ticketcreate-pr/SKILL.md} +0 -0
  146. /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272new.md" → skills/ticketnew/SKILL.md} +0 -0
  147. /package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/{.claude-plugin/commands/ticket/357/200/272review-commit.md" → skills/ticketreview-commit/SKILL.md} +0 -0
  148. /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
- Claude Code plugin that enforces working directory scope for all file operations. Blocks writes outside the current project directory and warns on reads outside it. Prevents accidental cross-project modifications in multi-project workspaces.
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 operations (Read, Write, Edit, NotebookEdit, Glob, Grep) and checks whether the target path is within the current working directory:
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) with error message |
12
- | Read, Glob, Grep | **Warned** (exit 0) with advisory context |
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
- When the current working directory is `/workspaces` (the workspace root), all operations are unrestricted.
16
+ ### Blacklisted Paths
15
17
 
16
- ### Allowed Prefixes
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 Code configuration |
23
- | `/workspaces/.tmp/` | Temporary files |
24
- | `/workspaces/.devcontainer/` | Container configuration |
30
+ | `/workspaces/.claude/` | Claude config, plans, rules |
25
31
  | `/tmp/` | System temp directory |
26
- | `/home/vscode/` | User home directory |
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
- ├─→ cwd is /workspaces? allow (unrestricted)
40
- ├─→ No target path? → allow (tool defaults to cwd)
41
- ├─→ Resolve path via os.path.realpath() (handles symlinks/worktrees)
42
- ├─→ Path is within cwd? → allow
43
- ├─→ Path matches allowed prefix? → allow
44
- ├─→ Write tool + out of scope exit 2 (block)
45
- └─→ Read tool + out of scope → exit 0 (warn via additionalContext)
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
- ### Symlink and Worktree Handling
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
- Target paths are resolved with `os.path.realpath()` before scope checking. This correctly handles:
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
- ### Path Field Mapping
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
- The script extracts the target path from different tool input fields:
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 | Fails open (exit 0) |
72
- | Other exceptions | Fails open (exit 0) — logs error to stderr |
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
- ### Timeout
139
+ ```json
140
+ {
141
+ "enabledPlugins": {
142
+ "workspace-scope-guard@<clone-path>/.devcontainer/plugins/devs-marketplace": true
143
+ }
144
+ }
145
+ ```
75
146
 
76
- The hook has a 5-second timeout.
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 # Plugin metadata
154
+ │ └── plugin.json # Plugin metadata
84
155
  ├── hooks/
85
- │ └── hooks.json # PreToolUse hook registration
156
+ │ └── hooks.json # Hook registrations (6 events)
86
157
  ├── scripts/
87
- └── guard-workspace-scope.py # Scope enforcement (PreToolUse)
88
- └── README.md # This file
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
@@ -1,14 +1,60 @@
1
1
  {
2
- "description": "Enforce workspace scope for file operations",
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": 5
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
- Enforce workspace scope for file operations.
3
+ Nuclear workspace scope enforcement.
4
4
 
5
- Blocks write operations (Write, Edit, NotebookEdit) to files outside the
6
- current working directory. Warns on read operations (Read, Glob, Grep)
7
- outside the working directory. Allows unrestricted access when cwd is
8
- /workspaces (the workspace root).
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 (with optional warning context).
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
- # Paths that are always allowed regardless of working directory
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/.tmp/",
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
- def is_in_scope(resolved_path: str, cwd: str) -> bool:
52
- """Check if resolved_path is within the working directory."""
53
- cwd_prefix = cwd if cwd.endswith("/") else cwd + "/"
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
- # Unrestricted when working from the workspace root
71
- if cwd == "/workspaces":
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 specified tool defaults to cwd, which is in scope
291
+ # No path tool defaults to cwd, always in scope (for known file tools)
77
292
  if target_path is None:
78
- sys.exit(0)
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
- if is_in_scope(resolved, cwd):
83
- sys.exit(0)
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
- json.dumps(
92
- {
93
- "error": (
94
- f"Blocked: {tool_name} targets '{target_path}' which is "
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
- if tool_name in READ_TOOLS:
104
- print(
105
- json.dumps(
106
- {
107
- "additionalContext": (
108
- f"Warning: {tool_name} targets '{target_path}' which is "
109
- f"outside the working directory ({cwd}). This read is "
110
- f"allowed but may indicate unintended cross-project access."
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
- # Unknown toolallow by default
118
- sys.exit(0)
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
- # Can't parse input — allow by default
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(0)
339
+ sys.exit(2)
127
340
 
128
341
 
129
342
  if __name__ == "__main__":