codeforge-dev 1.11.0 → 1.12.0
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/.env +7 -1
- package/.devcontainer/.gitignore +1 -0
- package/.devcontainer/CHANGELOG.md +69 -0
- package/.devcontainer/CLAUDE.md +73 -3
- package/.devcontainer/README.md +33 -7
- package/.devcontainer/config/defaults/main-system-prompt.md +28 -0
- package/.devcontainer/config/defaults/writing-system-prompt.md +46 -4
- package/.devcontainer/connect-external-terminal.ps1 +1 -1
- package/.devcontainer/devcontainer.json +32 -9
- package/.devcontainer/docs/configuration-reference.md +3 -0
- package/.devcontainer/docs/plugins.md +9 -2
- package/.devcontainer/docs/troubleshooting.md +2 -2
- package/.devcontainer/features/README.md +8 -9
- package/.devcontainer/features/agent-browser/devcontainer-feature.json +21 -21
- package/.devcontainer/features/agent-browser/install.sh +0 -7
- package/.devcontainer/features/ast-grep/devcontainer-feature.json +22 -22
- package/.devcontainer/features/biome/devcontainer-feature.json +12 -14
- package/.devcontainer/features/ccms/install.sh +30 -13
- package/.devcontainer/features/lsp-servers/devcontainer-feature.json +43 -43
- package/.devcontainer/features/mcp-qdrant/poststart-hook.sh +2 -1
- package/.devcontainer/features/ruff/devcontainer-feature.json +17 -19
- package/.devcontainer/features/tmux/install.sh +2 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-formatter/README.md +81 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/auto-linter/README.md +92 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/README.md +250 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/claude-guide.md +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/debug-logs.md +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/dependency-analyst.md +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/explorer.md +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/generalist.md +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/git-archaeologist.md +2 -2
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/researcher.md +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/security-auditor.md +1 -1
- package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/README.md +41 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/README.md +72 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/dangerous-command-blocker/scripts/block-dangerous.py +73 -47
- package/.devcontainer/plugins/devs-marketplace/plugins/notify-hook/README.md +42 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/README.md +86 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json +25 -15
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected-bash.py +122 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +3 -3
- package/.devcontainer/plugins/devs-marketplace/plugins/ticket-workflow/README.md +96 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/README.md +94 -0
- package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/guard-workspace-scope.py +1 -1
- package/.devcontainer/scripts/check-setup.sh +1 -1
- package/.devcontainer/scripts/setup-projects.sh +23 -16
- package/.devcontainer/scripts/setup.sh +48 -5
- package/README.md +17 -8
- package/package.json +1 -2
- package/.devcontainer/features/mcp-reasoner/README.md +0 -177
- package/.devcontainer/features/mcp-reasoner/devcontainer-feature.json +0 -25
- package/.devcontainer/features/mcp-reasoner/install.sh +0 -184
- package/.devcontainer/features/mcp-reasoner/poststart-hook.sh +0 -67
- package/.devcontainer/features/splitrail/README.md +0 -140
- package/.devcontainer/features/splitrail/devcontainer-feature.json +0 -39
- package/.devcontainer/features/splitrail/install.sh +0 -136
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# protected-files-guard
|
|
2
|
+
|
|
3
|
+
Claude Code plugin that blocks modifications to sensitive files — environment secrets, lock files, git internals, certificates, and credentials. Covers both direct file edits (Edit/Write tools) and indirect writes through Bash commands.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
Intercepts file operations and checks target paths against a set of protected patterns. If a match is found, the operation is blocked with an error message explaining why and suggesting the correct approach (e.g., "use npm install instead" for package-lock.json).
|
|
8
|
+
|
|
9
|
+
### Protected File Categories
|
|
10
|
+
|
|
11
|
+
| Category | Patterns | Reason |
|
|
12
|
+
|----------|----------|--------|
|
|
13
|
+
| Environment secrets | `.env`, `.env.*` | Contains secrets |
|
|
14
|
+
| Git internals | `.git/` | Managed by git |
|
|
15
|
+
| Lock files | `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `Gemfile.lock`, `poetry.lock`, `Cargo.lock`, `composer.lock`, `uv.lock` | Must be modified via package manager |
|
|
16
|
+
| Certificates & keys | `.pem`, `.key`, `.crt`, `.p12`, `.pfx` | Sensitive cryptographic material |
|
|
17
|
+
| Credential files | `credentials.json`, `secrets.yaml`, `secrets.yml`, `secrets.json`, `.secrets` | Contains secrets |
|
|
18
|
+
| Auth directories | `.ssh/`, `.aws/` | Contains authentication data |
|
|
19
|
+
| Auth config files | `.netrc`, `.npmrc`, `.pypirc` | Contains authentication credentials |
|
|
20
|
+
| SSH private keys | `id_rsa`, `id_ed25519`, `id_ecdsa` | SSH private key files |
|
|
21
|
+
|
|
22
|
+
## How It Works
|
|
23
|
+
|
|
24
|
+
### Two-Hook Architecture
|
|
25
|
+
|
|
26
|
+
The plugin registers two PreToolUse hooks to cover different attack vectors:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Claude calls Edit or Write tool
|
|
30
|
+
│
|
|
31
|
+
└─→ guard-protected.py checks file_path against protected patterns
|
|
32
|
+
│
|
|
33
|
+
├─→ Match → exit 2 (block)
|
|
34
|
+
└─→ No match → exit 0 (allow)
|
|
35
|
+
|
|
36
|
+
Claude calls Bash tool
|
|
37
|
+
│
|
|
38
|
+
└─→ guard-protected-bash.py extracts write targets from the command
|
|
39
|
+
│
|
|
40
|
+
├─→ Detects: > redirect, >> append, tee, cp, mv, sed -i, cat heredoc
|
|
41
|
+
├─→ Checks each target against protected patterns
|
|
42
|
+
├─→ Any match → exit 2 (block)
|
|
43
|
+
└─→ No match → exit 0 (allow)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Bash Write Detection
|
|
47
|
+
|
|
48
|
+
The Bash guard parses commands for write-indicating patterns and extracts the target file path:
|
|
49
|
+
|
|
50
|
+
| Pattern | Example |
|
|
51
|
+
|---------|---------|
|
|
52
|
+
| Redirect (`>`, `>>`) | `echo "key=val" > .env` |
|
|
53
|
+
| `tee` / `tee -a` | `cat data \| tee .env` |
|
|
54
|
+
| `cp` / `mv` | `cp template .env` |
|
|
55
|
+
| `sed -i` | `sed -i 's/old/new/' .env` |
|
|
56
|
+
| `cat` heredoc | `cat <<EOF > .env` |
|
|
57
|
+
|
|
58
|
+
### Error Handling
|
|
59
|
+
|
|
60
|
+
| Scenario | Behavior |
|
|
61
|
+
|----------|----------|
|
|
62
|
+
| JSON parse failure | Fails closed (exit 2) — blocks the operation |
|
|
63
|
+
| Other exceptions | Fails open (exit 0) — logs error, allows the operation |
|
|
64
|
+
|
|
65
|
+
### Timeout
|
|
66
|
+
|
|
67
|
+
Both hooks have a 5-second timeout.
|
|
68
|
+
|
|
69
|
+
## Plugin Structure
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
protected-files-guard/
|
|
73
|
+
├── .claude-plugin/
|
|
74
|
+
│ └── plugin.json # Plugin metadata
|
|
75
|
+
├── hooks/
|
|
76
|
+
│ └── hooks.json # PreToolUse hook registrations (Edit|Write + Bash)
|
|
77
|
+
├── scripts/
|
|
78
|
+
│ ├── guard-protected.py # Edit/Write file path checker
|
|
79
|
+
│ └── guard-protected-bash.py # Bash command write target checker
|
|
80
|
+
└── README.md # This file
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
- Python 3.11+
|
|
86
|
+
- Claude Code with plugin hook support
|
package/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/hooks/hooks.json
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
"description": "Block modifications to protected files",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "Edit|Write",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/guard-protected.py",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Bash",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/guard-protected-bash.py",
|
|
21
|
+
"timeout": 5
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
17
27
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Block bash commands that write to protected files.
|
|
4
|
+
|
|
5
|
+
Reads tool input from stdin, checks the command field for write operations
|
|
6
|
+
targeting protected file patterns.
|
|
7
|
+
Exit code 2 blocks the command with error message.
|
|
8
|
+
Exit code 0 allows the command to proceed.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# Same patterns as guard-protected.py
|
|
16
|
+
PROTECTED_PATTERNS = [
|
|
17
|
+
(r"(^|/)\.env$", "Blocked: .env contains secrets - edit manually if needed"),
|
|
18
|
+
(
|
|
19
|
+
r"(^|/)\.env\.[^/]+$",
|
|
20
|
+
"Blocked: .env.* files contain secrets - edit manually if needed",
|
|
21
|
+
),
|
|
22
|
+
(r"(^|/)\.git(/|$)", "Blocked: .git is managed by git"),
|
|
23
|
+
(
|
|
24
|
+
r"(^|/)package-lock\.json$",
|
|
25
|
+
"Blocked: package-lock.json - use npm install instead",
|
|
26
|
+
),
|
|
27
|
+
(r"(^|/)yarn\.lock$", "Blocked: yarn.lock - use yarn install instead"),
|
|
28
|
+
(r"(^|/)pnpm-lock\.yaml$", "Blocked: pnpm-lock.yaml - use pnpm install instead"),
|
|
29
|
+
(r"(^|/)Gemfile\.lock$", "Blocked: Gemfile.lock - use bundle install instead"),
|
|
30
|
+
(r"(^|/)poetry\.lock$", "Blocked: poetry.lock - use poetry install instead"),
|
|
31
|
+
(r"(^|/)Cargo\.lock$", "Blocked: Cargo.lock - use cargo build instead"),
|
|
32
|
+
(r"(^|/)composer\.lock$", "Blocked: composer.lock - use composer install instead"),
|
|
33
|
+
(r"(^|/)uv\.lock$", "Blocked: uv.lock - use uv sync instead"),
|
|
34
|
+
(r"\.pem$", "Blocked: .pem files contain sensitive cryptographic material"),
|
|
35
|
+
(r"\.key$", "Blocked: .key files contain sensitive cryptographic material"),
|
|
36
|
+
(r"\.crt$", "Blocked: .crt certificate files should not be edited directly"),
|
|
37
|
+
(r"\.p12$", "Blocked: .p12 files contain sensitive cryptographic material"),
|
|
38
|
+
(r"\.pfx$", "Blocked: .pfx files contain sensitive cryptographic material"),
|
|
39
|
+
(r"(^|/)credentials\.json$", "Blocked: credentials.json contains secrets"),
|
|
40
|
+
(r"(^|/)secrets\.yaml$", "Blocked: secrets.yaml contains secrets"),
|
|
41
|
+
(r"(^|/)secrets\.yml$", "Blocked: secrets.yml contains secrets"),
|
|
42
|
+
(r"(^|/)secrets\.json$", "Blocked: secrets.json contains secrets"),
|
|
43
|
+
(r"(^|/)\.secrets$", "Blocked: .secrets file contains secrets"),
|
|
44
|
+
(r"(^|/)\.ssh/", "Blocked: .ssh/ contains sensitive authentication data"),
|
|
45
|
+
(r"(^|/)\.aws/", "Blocked: .aws/ contains AWS credentials"),
|
|
46
|
+
(r"(^|/)\.netrc$", "Blocked: .netrc contains authentication credentials"),
|
|
47
|
+
(
|
|
48
|
+
r"(^|/)\.npmrc$",
|
|
49
|
+
"Blocked: .npmrc may contain auth tokens - edit manually if needed",
|
|
50
|
+
),
|
|
51
|
+
(r"(^|/)\.pypirc$", "Blocked: .pypirc contains PyPI credentials"),
|
|
52
|
+
(r"(^|/|-)id_rsa($|\.)", "Blocked: SSH private key file"),
|
|
53
|
+
(r"(^|/)id_ed25519", "Blocked: SSH private key file"),
|
|
54
|
+
(r"(^|/)id_ecdsa", "Blocked: SSH private key file"),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
# Patterns that indicate a bash command is writing to a file
|
|
58
|
+
# Each captures the target file path for checking against PROTECTED_PATTERNS
|
|
59
|
+
WRITE_PATTERNS = [
|
|
60
|
+
# Redirect: > file, >> file
|
|
61
|
+
r"(?:>|>>)\s*([^\s;&|]+)",
|
|
62
|
+
# tee: tee file, tee -a file
|
|
63
|
+
r"\btee\s+(?:-a\s+)?([^\s;&|]+)",
|
|
64
|
+
# cp/mv: cp src dest, mv src dest
|
|
65
|
+
r"\b(?:cp|mv)\s+(?:-[^\s]+\s+)*[^\s]+\s+([^\s;&|]+)",
|
|
66
|
+
# sed -i: sed -i '' file
|
|
67
|
+
r'\bsed\s+-i[^\s]*\s+(?:\'[^\']*\'\s+|"[^"]*"\s+|[^\s]+\s+)*([^\s;&|]+)',
|
|
68
|
+
# cat > file (heredoc style)
|
|
69
|
+
r"\bcat\s+(?:<<[^\s]*\s+)?>\s*([^\s;&|]+)",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extract_write_targets(command: str) -> list[str]:
|
|
74
|
+
"""Extract file paths that the command writes to."""
|
|
75
|
+
targets = []
|
|
76
|
+
for pattern in WRITE_PATTERNS:
|
|
77
|
+
for match in re.finditer(pattern, command):
|
|
78
|
+
target = match.group(1).strip("'\"")
|
|
79
|
+
if target:
|
|
80
|
+
targets.append(target)
|
|
81
|
+
return targets
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_path(file_path: str) -> tuple[bool, str]:
|
|
85
|
+
"""Check if file path matches any protected pattern."""
|
|
86
|
+
normalized = file_path.replace("\\", "/")
|
|
87
|
+
for pattern, message in PROTECTED_PATTERNS:
|
|
88
|
+
if re.search(pattern, normalized, re.IGNORECASE):
|
|
89
|
+
return True, message
|
|
90
|
+
return False, ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main():
|
|
94
|
+
try:
|
|
95
|
+
input_data = json.load(sys.stdin)
|
|
96
|
+
tool_input = input_data.get("tool_input", {})
|
|
97
|
+
command = tool_input.get("command", "")
|
|
98
|
+
|
|
99
|
+
if not command:
|
|
100
|
+
sys.exit(0)
|
|
101
|
+
|
|
102
|
+
targets = extract_write_targets(command)
|
|
103
|
+
|
|
104
|
+
for target in targets:
|
|
105
|
+
is_protected, message = check_path(target)
|
|
106
|
+
if is_protected:
|
|
107
|
+
print(json.dumps({"error": f"{message} (via bash command)"}))
|
|
108
|
+
sys.exit(2)
|
|
109
|
+
|
|
110
|
+
sys.exit(0)
|
|
111
|
+
|
|
112
|
+
except json.JSONDecodeError:
|
|
113
|
+
# Fail closed: can't parse means can't verify safety
|
|
114
|
+
sys.exit(2)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
# Log error but don't block on hook failure
|
|
117
|
+
print(f"Hook error: {e}", file=sys.stderr)
|
|
118
|
+
sys.exit(0)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
main()
|
|
@@ -20,7 +20,7 @@ PROTECTED_PATTERNS = [
|
|
|
20
20
|
"Blocked: .env.* files contain secrets - edit manually if needed",
|
|
21
21
|
),
|
|
22
22
|
# Git internals
|
|
23
|
-
(r"(^|/)\.git
|
|
23
|
+
(r"(^|/)\.git(/|$)", "Blocked: .git is managed by git"),
|
|
24
24
|
# Lock files (should be modified via package manager)
|
|
25
25
|
(
|
|
26
26
|
r"(^|/)package-lock\.json$",
|
|
@@ -97,8 +97,8 @@ def main():
|
|
|
97
97
|
sys.exit(0)
|
|
98
98
|
|
|
99
99
|
except json.JSONDecodeError:
|
|
100
|
-
#
|
|
101
|
-
sys.exit(
|
|
100
|
+
# Fail closed: can't parse means can't verify safety
|
|
101
|
+
sys.exit(2)
|
|
102
102
|
except Exception as e:
|
|
103
103
|
# Log error but don't block on hook failure
|
|
104
104
|
print(f"Hook error: {e}", file=sys.stderr)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# ticket-workflow
|
|
2
|
+
|
|
3
|
+
Claude Code plugin that provides an EARS-based ticket workflow with GitHub issues as the single source of truth. Command-driven — no hooks or scripts, just a custom system prompt and four slash commands.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
Provides a structured workflow for creating, planning, reviewing, and shipping work through GitHub issues. All major decisions, plans, and progress are posted as issue comments to maintain an audit trail.
|
|
8
|
+
|
|
9
|
+
### Slash Commands
|
|
10
|
+
|
|
11
|
+
| Command | Description |
|
|
12
|
+
|---------|-------------|
|
|
13
|
+
| `/ticket:new` | Transform requirements into a structured GitHub issue with EARS-formatted business requirements |
|
|
14
|
+
| `/ticket:work` | Retrieve a ticket, create a technical implementation plan, and post it to the GitHub issue |
|
|
15
|
+
| `/ticket:review-commit` | Conduct a thorough code review, verify requirements are met, and commit with a detailed message |
|
|
16
|
+
| `/ticket:create-pr` | Create a pull request with aggressive security and architecture review |
|
|
17
|
+
|
|
18
|
+
### EARS Requirement Format
|
|
19
|
+
|
|
20
|
+
Every requirement uses one of these patterns:
|
|
21
|
+
|
|
22
|
+
| Type | Template |
|
|
23
|
+
|------|----------|
|
|
24
|
+
| Ubiquitous | The `<system>` shall `<response>`. |
|
|
25
|
+
| Event-Driven | WHEN `<trigger>`, the `<system>` shall `<response>`. |
|
|
26
|
+
| State-Driven | WHILE `<state>`, the `<system>` shall `<response>`. |
|
|
27
|
+
| Unwanted Behavior | IF `<condition>`, THEN the `<system>` shall `<response>`. |
|
|
28
|
+
| Optional Feature | WHERE `<feature>`, the `<system>` shall `<response>`. |
|
|
29
|
+
|
|
30
|
+
## How It Works
|
|
31
|
+
|
|
32
|
+
### Workflow Lifecycle
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
/ticket:new [requirements]
|
|
36
|
+
│
|
|
37
|
+
└─→ Gather requirements → Create EARS-formatted GitHub issue
|
|
38
|
+
│
|
|
39
|
+
└─→ /ticket:work #123
|
|
40
|
+
│
|
|
41
|
+
└─→ Fetch issue → Create technical plan → Post plan as issue comment
|
|
42
|
+
│
|
|
43
|
+
│ ... implementation work ...
|
|
44
|
+
│
|
|
45
|
+
└─→ /ticket:review-commit
|
|
46
|
+
│
|
|
47
|
+
└─→ Review changes → Verify requirements → Commit
|
|
48
|
+
│
|
|
49
|
+
└─→ /ticket:create-pr
|
|
50
|
+
│
|
|
51
|
+
└─→ Create PR → Security/architecture review → Post findings
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Ticket Structure
|
|
55
|
+
|
|
56
|
+
Each ticket created by `/ticket:new` includes:
|
|
57
|
+
- **Overview**: Plain language description
|
|
58
|
+
- **Requirements**: EARS-formatted business requirements
|
|
59
|
+
- **Technical Questions**: Open questions for implementation
|
|
60
|
+
- **Acceptance Criteria**: Verifiable conditions for completion
|
|
61
|
+
|
|
62
|
+
### Audit Trail
|
|
63
|
+
|
|
64
|
+
| Action | Destination |
|
|
65
|
+
|--------|-------------|
|
|
66
|
+
| Plans | Issue comment |
|
|
67
|
+
| Decisions | Issue comment |
|
|
68
|
+
| Requirement changes | Issue comment |
|
|
69
|
+
| Commit summaries | Issue comment |
|
|
70
|
+
| Review findings | PR + issue comment |
|
|
71
|
+
| Created sub-issues | Linked to source ticket |
|
|
72
|
+
|
|
73
|
+
### Custom System Prompt
|
|
74
|
+
|
|
75
|
+
The plugin injects a system prompt that defines the assistant persona, coding standards (SOLID, DRY, KISS, YAGNI), testing standards, and the ticket workflow rules. This ensures consistent behavior across all four commands.
|
|
76
|
+
|
|
77
|
+
## Plugin Structure
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
ticket-workflow/
|
|
81
|
+
├── .claude-plugin/
|
|
82
|
+
│ ├── plugin.json # Plugin metadata
|
|
83
|
+
│ ├── system-prompt.md # Custom system prompt (persona + workflow rules)
|
|
84
|
+
│ └── commands/
|
|
85
|
+
│ ├── ticket:new.md # Create EARS-formatted issue
|
|
86
|
+
│ ├── ticket:work.md # Implementation planning
|
|
87
|
+
│ ├── ticket:review-commit.md # Review and commit
|
|
88
|
+
│ └── ticket:create-pr.md # PR creation with review
|
|
89
|
+
└── README.md # This file
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Requirements
|
|
93
|
+
|
|
94
|
+
- Claude Code with plugin command support
|
|
95
|
+
- [GitHub CLI](https://cli.github.com/) (`gh`) installed and authenticated
|
|
96
|
+
- A GitHub repository as the working context
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# workspace-scope-guard
|
|
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.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
Intercepts file operations (Read, Write, Edit, NotebookEdit, Glob, Grep) and checks whether the target path is within the current working directory:
|
|
8
|
+
|
|
9
|
+
| Operation | Out-of-scope behavior |
|
|
10
|
+
|-----------|-----------------------|
|
|
11
|
+
| Write, Edit, NotebookEdit | **Blocked** (exit 2) with error message |
|
|
12
|
+
| Read, Glob, Grep | **Warned** (exit 0) with advisory context |
|
|
13
|
+
|
|
14
|
+
When the current working directory is `/workspaces` (the workspace root), all operations are unrestricted.
|
|
15
|
+
|
|
16
|
+
### Allowed Prefixes
|
|
17
|
+
|
|
18
|
+
These paths are always permitted regardless of working directory:
|
|
19
|
+
|
|
20
|
+
| Path | Reason |
|
|
21
|
+
|------|--------|
|
|
22
|
+
| `/workspaces/.claude/` | Claude Code configuration |
|
|
23
|
+
| `/workspaces/.tmp/` | Temporary files |
|
|
24
|
+
| `/workspaces/.devcontainer/` | Container configuration |
|
|
25
|
+
| `/tmp/` | System temp directory |
|
|
26
|
+
| `/home/vscode/` | User home directory |
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
### Hook Lifecycle
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Claude calls Read, Write, Edit, NotebookEdit, Glob, or Grep
|
|
34
|
+
│
|
|
35
|
+
└─→ PreToolUse hook fires
|
|
36
|
+
│
|
|
37
|
+
└─→ guard-workspace-scope.py
|
|
38
|
+
│
|
|
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)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Symlink and Worktree Handling
|
|
49
|
+
|
|
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:`)
|
|
53
|
+
|
|
54
|
+
### Path Field Mapping
|
|
55
|
+
|
|
56
|
+
The script extracts the target path from different tool input fields:
|
|
57
|
+
|
|
58
|
+
| Tool | Input Field |
|
|
59
|
+
|------|-------------|
|
|
60
|
+
| Read | `file_path` |
|
|
61
|
+
| Write | `file_path` |
|
|
62
|
+
| Edit | `file_path` |
|
|
63
|
+
| NotebookEdit | `notebook_path` |
|
|
64
|
+
| Glob | `path` |
|
|
65
|
+
| Grep | `path` |
|
|
66
|
+
|
|
67
|
+
### Error Handling
|
|
68
|
+
|
|
69
|
+
| Scenario | Behavior |
|
|
70
|
+
|----------|----------|
|
|
71
|
+
| JSON parse failure | Fails open (exit 0) |
|
|
72
|
+
| Other exceptions | Fails open (exit 0) — logs error to stderr |
|
|
73
|
+
|
|
74
|
+
### Timeout
|
|
75
|
+
|
|
76
|
+
The hook has a 5-second timeout.
|
|
77
|
+
|
|
78
|
+
## Plugin Structure
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
workspace-scope-guard/
|
|
82
|
+
├── .claude-plugin/
|
|
83
|
+
│ └── plugin.json # Plugin metadata
|
|
84
|
+
├── hooks/
|
|
85
|
+
│ └── hooks.json # PreToolUse hook registration
|
|
86
|
+
├── scripts/
|
|
87
|
+
│ └── guard-workspace-scope.py # Scope enforcement (PreToolUse)
|
|
88
|
+
└── README.md # This file
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Requirements
|
|
92
|
+
|
|
93
|
+
- Python 3.11+
|
|
94
|
+
- Claude Code with plugin hook support
|
|
@@ -34,7 +34,7 @@ warn_check() {
|
|
|
34
34
|
echo ""
|
|
35
35
|
echo "Core:"
|
|
36
36
|
check "Claude Code installed" "command -v claude"
|
|
37
|
-
warn_check "Claude native binary" "[ -x /usr/local/bin/claude ]"
|
|
37
|
+
warn_check "Claude native binary" "[ -x ~/.local/bin/claude ] || [ -x /usr/local/bin/claude ]"
|
|
38
38
|
check "cc alias configured" "grep -q 'alias cc=' ~/.bashrc 2>/dev/null || grep -q 'alias cc=' ~/.zshrc 2>/dev/null"
|
|
39
39
|
check "Config directory exists" "[ -d '${CLAUDE_CONFIG_DIR:-/workspaces/.claude}' ]"
|
|
40
40
|
check "Settings file exists" "[ -f '${CLAUDE_CONFIG_DIR:-/workspaces/.claude}/settings.json' ]"
|
|
@@ -29,7 +29,7 @@ is_excluded() {
|
|
|
29
29
|
|
|
30
30
|
has_project_markers() {
|
|
31
31
|
local dir="$1"
|
|
32
|
-
[ -d "$dir/.git" ] || [ -f "$dir/package.json" ] || [ -f "$dir/pyproject.toml" ] ||
|
|
32
|
+
[ -d "$dir/.git" ] || [ -f "$dir/.git" ] || [ -f "$dir/package.json" ] || [ -f "$dir/pyproject.toml" ] ||
|
|
33
33
|
[ -f "$dir/Cargo.toml" ] || [ -f "$dir/go.mod" ] || [ -f "$dir/deno.json" ] ||
|
|
34
34
|
[ -f "$dir/Makefile" ] || [ -f "$dir/CLAUDE.md" ]
|
|
35
35
|
}
|
|
@@ -38,7 +38,11 @@ detect_tags() {
|
|
|
38
38
|
local dir="$1"
|
|
39
39
|
local tags=()
|
|
40
40
|
|
|
41
|
-
[ -
|
|
41
|
+
if [ -f "$dir/.git" ] && grep -q "gitdir:" "$dir/.git" 2>/dev/null; then
|
|
42
|
+
tags+=("git" "worktree")
|
|
43
|
+
elif [ -d "$dir/.git" ]; then
|
|
44
|
+
tags+=("git")
|
|
45
|
+
fi
|
|
42
46
|
[ -f "$dir/package.json" ] && tags+=("node")
|
|
43
47
|
[ -f "$dir/pyproject.toml" ] && tags+=("python")
|
|
44
48
|
[ -f "$dir/Cargo.toml" ] && tags+=("rust")
|
|
@@ -95,6 +99,19 @@ scan_and_update() {
|
|
|
95
99
|
is_excluded "$subname" && continue
|
|
96
100
|
new_projects=$(register_project "$new_projects" "$subname" "$subdir")
|
|
97
101
|
done
|
|
102
|
+
|
|
103
|
+
# Depth 3: .worktrees/ is hidden (not matched by */) — scan explicitly
|
|
104
|
+
local wtcontainer="${dir%/}/.worktrees"
|
|
105
|
+
if [ -d "$wtcontainer" ]; then
|
|
106
|
+
for wtdir in "${wtcontainer%/}"/*/; do
|
|
107
|
+
[ -d "$wtdir" ] || continue
|
|
108
|
+
local wtname
|
|
109
|
+
wtname=$(basename "$wtdir")
|
|
110
|
+
if has_project_markers "$wtdir"; then
|
|
111
|
+
new_projects=$(register_project "$new_projects" "$wtname" "$wtdir")
|
|
112
|
+
fi
|
|
113
|
+
done
|
|
114
|
+
fi
|
|
98
115
|
fi
|
|
99
116
|
done
|
|
100
117
|
|
|
@@ -158,20 +175,10 @@ start_watcher() {
|
|
|
158
175
|
stop_watcher
|
|
159
176
|
fi
|
|
160
177
|
|
|
161
|
-
# Check if inotifywait is available
|
|
178
|
+
# Check if inotifywait is available (installed by tmux feature at build time)
|
|
162
179
|
if ! command -v inotifywait &>/dev/null; then
|
|
163
|
-
echo "$LOG_PREFIX
|
|
164
|
-
|
|
165
|
-
sudo apt-get update -qq && sudo apt-get install -y -qq inotify-tools >/dev/null 2>&1
|
|
166
|
-
else
|
|
167
|
-
apt-get update -qq && apt-get install -y -qq inotify-tools >/dev/null 2>&1
|
|
168
|
-
fi
|
|
169
|
-
|
|
170
|
-
if ! command -v inotifywait &>/dev/null; then
|
|
171
|
-
echo "$LOG_PREFIX WARNING: Could not install inotify-tools, watcher disabled"
|
|
172
|
-
return 1
|
|
173
|
-
fi
|
|
174
|
-
echo "$LOG_PREFIX inotify-tools installed"
|
|
180
|
+
echo "$LOG_PREFIX WARNING: inotify-tools not installed, watcher disabled"
|
|
181
|
+
return 1
|
|
175
182
|
fi
|
|
176
183
|
|
|
177
184
|
# Fork background watcher in its own process group for clean shutdown
|
|
@@ -181,7 +188,7 @@ start_watcher() {
|
|
|
181
188
|
# -r watches subdirectories (catches events inside container dirs like projects/)
|
|
182
189
|
# --exclude filters noisy dirs that generate frequent irrelevant events
|
|
183
190
|
inotifywait -m -r -q -e create,delete,moved_to,moved_from \
|
|
184
|
-
--exclude '(node_modules|\.git
|
|
191
|
+
--exclude '(node_modules|\.git|\.tmp|__pycache__|\.venv)' \
|
|
185
192
|
--format '%w%f %e' "$WORKSPACE_ROOT" 2>/dev/null |
|
|
186
193
|
while read -r _path event; do
|
|
187
194
|
# Small delay to let filesystem settle (e.g., move operations)
|
|
@@ -22,8 +22,9 @@ fi
|
|
|
22
22
|
: "${SETUP_UPDATE_CLAUDE:=true}"
|
|
23
23
|
: "${SETUP_PROJECTS:=true}"
|
|
24
24
|
: "${SETUP_TERMINAL:=true}"
|
|
25
|
+
: "${SETUP_POSTSTART:=true}"
|
|
25
26
|
|
|
26
|
-
export CLAUDE_CONFIG_DIR CONFIG_SOURCE_DIR SETUP_CONFIG SETUP_ALIASES SETUP_AUTH SETUP_PLUGINS SETUP_UPDATE_CLAUDE SETUP_PROJECTS SETUP_TERMINAL
|
|
27
|
+
export CLAUDE_CONFIG_DIR CONFIG_SOURCE_DIR SETUP_CONFIG SETUP_ALIASES SETUP_AUTH SETUP_PLUGINS SETUP_UPDATE_CLAUDE SETUP_PROJECTS SETUP_TERMINAL SETUP_POSTSTART
|
|
27
28
|
|
|
28
29
|
SETUP_START=$(date +%s)
|
|
29
30
|
SETUP_RESULTS=()
|
|
@@ -42,12 +43,16 @@ run_script() {
|
|
|
42
43
|
if [ "$enabled" = "true" ]; then
|
|
43
44
|
if [ -f "$script" ]; then
|
|
44
45
|
printf " %-30s" "$name..."
|
|
45
|
-
|
|
46
|
+
local output
|
|
47
|
+
if output=$(bash "$script" 2>&1); then
|
|
46
48
|
echo "done"
|
|
47
49
|
SETUP_RESULTS+=("$name:ok")
|
|
48
50
|
else
|
|
49
|
-
|
|
51
|
+
local exit_code=$?
|
|
52
|
+
echo "FAILED (exit $exit_code)"
|
|
50
53
|
SETUP_RESULTS+=("$name:failed")
|
|
54
|
+
# Show output on failure for diagnostics
|
|
55
|
+
echo "$output" | sed 's/^/ /'
|
|
51
56
|
fi
|
|
52
57
|
else
|
|
53
58
|
echo " $name... not found, skipping"
|
|
@@ -59,6 +64,30 @@ run_script() {
|
|
|
59
64
|
fi
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
run_poststart_hooks() {
|
|
68
|
+
local hook_dir="/usr/local/devcontainer-poststart.d"
|
|
69
|
+
if [ ! -d "$hook_dir" ]; then
|
|
70
|
+
return 0
|
|
71
|
+
fi
|
|
72
|
+
local count=0
|
|
73
|
+
for hook in "$hook_dir"/*.sh; do
|
|
74
|
+
[ -f "$hook" ] || continue
|
|
75
|
+
[ -x "$hook" ] || continue
|
|
76
|
+
local name
|
|
77
|
+
name="$(basename "$hook")"
|
|
78
|
+
printf " %-30s" "$name..."
|
|
79
|
+
if bash "$hook" 2>&1; then
|
|
80
|
+
echo "done"
|
|
81
|
+
count=$((count + 1))
|
|
82
|
+
else
|
|
83
|
+
echo "FAILED (exit $?)"
|
|
84
|
+
fi
|
|
85
|
+
done
|
|
86
|
+
if [ $count -gt 0 ]; then
|
|
87
|
+
SETUP_RESULTS+=("poststart-hooks:ok ($count)")
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
|
|
62
91
|
run_script "$SCRIPT_DIR/setup-symlink-claude.sh" "true"
|
|
63
92
|
run_script "$SCRIPT_DIR/setup-auth.sh" "$SETUP_AUTH"
|
|
64
93
|
run_script "$SCRIPT_DIR/setup-config.sh" "$SETUP_CONFIG"
|
|
@@ -66,7 +95,20 @@ run_script "$SCRIPT_DIR/setup-aliases.sh" "$SETUP_ALIASES"
|
|
|
66
95
|
run_script "$SCRIPT_DIR/setup-plugins.sh" "$SETUP_PLUGINS"
|
|
67
96
|
run_script "$SCRIPT_DIR/setup-projects.sh" "$SETUP_PROJECTS"
|
|
68
97
|
run_script "$SCRIPT_DIR/setup-terminal.sh" "$SETUP_TERMINAL"
|
|
69
|
-
|
|
98
|
+
|
|
99
|
+
# Background the update to avoid blocking container start
|
|
100
|
+
if [ "$SETUP_UPDATE_CLAUDE" = "true" ] && [ -f "$SCRIPT_DIR/setup-update-claude.sh" ]; then
|
|
101
|
+
bash "$SCRIPT_DIR/setup-update-claude.sh" &>/dev/null &
|
|
102
|
+
disown
|
|
103
|
+
SETUP_RESULTS+=("setup-update-claude:background")
|
|
104
|
+
else
|
|
105
|
+
SETUP_RESULTS+=("setup-update-claude:disabled")
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Run post-start hooks
|
|
109
|
+
if [ "$SETUP_POSTSTART" = "true" ]; then
|
|
110
|
+
run_poststart_hooks
|
|
111
|
+
fi
|
|
70
112
|
|
|
71
113
|
echo ""
|
|
72
114
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -77,10 +119,11 @@ for result in "${SETUP_RESULTS[@]}"; do
|
|
|
77
119
|
name="${result%%:*}"
|
|
78
120
|
status="${result##*:}"
|
|
79
121
|
case "$status" in
|
|
80
|
-
ok)
|
|
122
|
+
ok*) printf " ✓ %s\n" "$name" ;;
|
|
81
123
|
failed) printf " ✗ %s (FAILED)\n" "$name"; FAILURES=$((FAILURES + 1)) ;;
|
|
82
124
|
disabled) printf " - %s (disabled)\n" "$name" ;;
|
|
83
125
|
missing) printf " ? %s (not found)\n" "$name" ;;
|
|
126
|
+
background) printf " ⇢ %s (background)\n" "$name" ;;
|
|
84
127
|
esac
|
|
85
128
|
done
|
|
86
129
|
ELAPSED=$(( $(date +%s) - SETUP_START ))
|