@wipcomputer/wip-file-guard 1.0.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/CHANGELOG.md +6 -0
- package/README.md +105 -0
- package/REFERENCE.md +79 -0
- package/SKILL.md +98 -0
- package/guard.mjs +128 -0
- package/openclaw.plugin.json +8 -0
- package/package.json +27 -0
- package/test.sh +119 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
###### WIP Computer
|
|
2
|
+
# File Guard
|
|
3
|
+
|
|
4
|
+
PreToolUse hook that blocks destructive edits to protected files. When an AI agent tries to overwrite or strip content from files like CLAUDE.md, SHARED-CONTEXT.md, or SOUL.md... it gets blocked with a clear explanation of what went wrong.
|
|
5
|
+
|
|
6
|
+
## The Problem
|
|
7
|
+
|
|
8
|
+
AI agents replace content instead of extending it. After context compaction, behavioral rules like "don't delete things" vanish. The agent rewrites your CLAUDE.md, strips 30 lines from SHARED-CONTEXT.md, or replaces your SOUL.md with a shorter version. Every time.
|
|
9
|
+
|
|
10
|
+
File Guard is a technical guardrail. It doesn't ask the agent to be careful. It blocks the operation before it happens.
|
|
11
|
+
|
|
12
|
+
## How It Works
|
|
13
|
+
|
|
14
|
+
Two rules:
|
|
15
|
+
|
|
16
|
+
1. **Write is blocked** on protected files. Always. Use Edit instead.
|
|
17
|
+
2. **Edit is blocked** when it removes more than 2 net lines from a protected file.
|
|
18
|
+
|
|
19
|
+
The agent gets a deny message explaining what happened and telling it to re-read the file and add content instead of replacing it.
|
|
20
|
+
|
|
21
|
+
### Protected Files
|
|
22
|
+
|
|
23
|
+
| File | What it protects |
|
|
24
|
+
|------|-----------------|
|
|
25
|
+
| `CLAUDE.md` | Project instructions, boot sequence, system docs |
|
|
26
|
+
| `SHARED-CONTEXT.md` | Cross-agent shared state |
|
|
27
|
+
| `SOUL.md` | Agent identity |
|
|
28
|
+
| `IDENTITY.md` | Agent identity (alternate format) |
|
|
29
|
+
| `CONTEXT.md` | Current state snapshot |
|
|
30
|
+
| `TOOLS.md` | Tool and workflow rules |
|
|
31
|
+
| `MEMORY.md` | Persistent memory and preferences |
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
Open your AI coding tool and say:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Read the README at github.com/wipcomputer/wip-file-guard.
|
|
39
|
+
Then explain to me:
|
|
40
|
+
1. What is this tool?
|
|
41
|
+
2. What does it do?
|
|
42
|
+
3. What would it change or fix in our current system?
|
|
43
|
+
|
|
44
|
+
Then ask me:
|
|
45
|
+
- Do you have more questions?
|
|
46
|
+
- Do you want to integrate it into our system?
|
|
47
|
+
- Do you want to clone it (use as-is) or fork it (so you can contribute back if you find bugs)?
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Your agent will read the repo, explain the tool, and walk you through integration interactively.
|
|
51
|
+
|
|
52
|
+
Also see **[wip-release](https://github.com/wipcomputer/wip-release)** ... one-command release pipeline for agent-native software.
|
|
53
|
+
|
|
54
|
+
See [REFERENCE.md](REFERENCE.md) for manual install instructions (Claude Code, OpenClaw, CLI).
|
|
55
|
+
|
|
56
|
+
## Four Interfaces
|
|
57
|
+
|
|
58
|
+
One core, four interfaces into the same guard logic.
|
|
59
|
+
|
|
60
|
+
| Interface | File | What it does |
|
|
61
|
+
|-----------|------|-------------|
|
|
62
|
+
| **Core** | `guard.mjs` | Pure guard logic. Reads stdin JSON, decides allow/deny. |
|
|
63
|
+
| **Claude Code** | `guard.mjs` (PreToolUse hook) | Hooks into CC's PreToolUse event. Blocks before the edit happens. |
|
|
64
|
+
| **OpenClaw** | `openclaw.plugin.json` | Lifecycle hook for OpenClaw agents. Same rules, different runtime. |
|
|
65
|
+
| **CLI** | `guard.mjs --list`, `test.sh` | Testing and inspection from the command line. |
|
|
66
|
+
|
|
67
|
+
See [REFERENCE.md](REFERENCE.md) for customization (adding protected files, changing thresholds).
|
|
68
|
+
|
|
69
|
+
## Tests
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bash test.sh
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
wip-file-guard tests
|
|
77
|
+
===================
|
|
78
|
+
|
|
79
|
+
PASS: Block Write to CLAUDE.md
|
|
80
|
+
PASS: Block Write to SHARED-CONTEXT.md
|
|
81
|
+
PASS: Allow Write to random file
|
|
82
|
+
PASS: Block Edit removing 5 lines from CLAUDE.md
|
|
83
|
+
PASS: Allow Edit adding lines to CLAUDE.md
|
|
84
|
+
PASS: Allow Edit on non-protected file (even removing lines)
|
|
85
|
+
PASS: Allow Edit with small removal (2 lines)
|
|
86
|
+
PASS: Block Edit with 4 line removal from SOUL.md
|
|
87
|
+
PASS: Block Write to IDENTITY.md
|
|
88
|
+
PASS: Block Write to TOOLS.md
|
|
89
|
+
|
|
90
|
+
Results: 10 passed, 0 failed
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Why This Exists
|
|
94
|
+
|
|
95
|
+
Context compaction erases behavioral rules. An agent that was told "never delete content from CLAUDE.md" forgets that instruction after compaction. It then proceeds to replace 50 lines with 10, confident it's improving the file.
|
|
96
|
+
|
|
97
|
+
This happened five times in one session. The fix isn't better prompting. It's a hook that blocks the operation before it executes. Behavioral rules degrade. Technical guards don't.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
104
|
+
|
|
105
|
+
Built by Parker Todd Brooks, with Claude Code and Lēsa (OpenClaw).
|
package/REFERENCE.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
###### WIP Computer
|
|
2
|
+
# wip-file-guard ... Reference
|
|
3
|
+
|
|
4
|
+
Manual install instructions, CLI usage, and customization.
|
|
5
|
+
|
|
6
|
+
## Claude Code
|
|
7
|
+
|
|
8
|
+
Add to `~/.claude/settings.json`:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"hooks": {
|
|
13
|
+
"PreToolUse": [
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Edit|Write",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "node \"/path/to/wip-file-guard/guard.mjs\"",
|
|
20
|
+
"timeout": 5
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Replace `/path/to/wip-file-guard/` with where you cloned the repo.
|
|
30
|
+
|
|
31
|
+
## OpenClaw
|
|
32
|
+
|
|
33
|
+
Add to your OpenClaw installation's `extensions/` directory:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cp -r wip-file-guard ~/.openclaw/extensions/wip-file-guard
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The `openclaw.plugin.json` registers a `before_tool_use` lifecycle hook that applies the same rules.
|
|
40
|
+
|
|
41
|
+
## CLI
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# List protected files
|
|
45
|
+
node guard.mjs --list
|
|
46
|
+
|
|
47
|
+
# Test the guard with a simulated input
|
|
48
|
+
echo '{"tool_name":"Write","tool_input":{"file_path":"/foo/CLAUDE.md"}}' | node guard.mjs
|
|
49
|
+
|
|
50
|
+
# Run the test suite
|
|
51
|
+
bash test.sh
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Customization
|
|
55
|
+
|
|
56
|
+
### Adding Protected Files
|
|
57
|
+
|
|
58
|
+
Edit the `PROTECTED` set in `guard.mjs`:
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
const PROTECTED = new Set([
|
|
62
|
+
'CLAUDE.md',
|
|
63
|
+
'SHARED-CONTEXT.md',
|
|
64
|
+
'SOUL.md',
|
|
65
|
+
'IDENTITY.md',
|
|
66
|
+
'CONTEXT.md',
|
|
67
|
+
'TOOLS.md',
|
|
68
|
+
'MEMORY.md',
|
|
69
|
+
'YOUR-FILE-HERE.md', // add yours
|
|
70
|
+
]);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Changing the Line Threshold
|
|
74
|
+
|
|
75
|
+
The default blocks edits that remove more than 2 net lines. Change the threshold in the Edit handler:
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
if (removed > 2) { // change 2 to your threshold
|
|
79
|
+
```
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: WIP.file-guard
|
|
3
|
+
version: 1.0.1
|
|
4
|
+
description: Hook that blocks destructive edits to protected identity files. For Claude Code CLI and OpenClaw.
|
|
5
|
+
homepage: https://github.com/wipcomputer/wip-file-guard
|
|
6
|
+
metadata:
|
|
7
|
+
category: dev-tools
|
|
8
|
+
capabilities:
|
|
9
|
+
- file-protection
|
|
10
|
+
- edit-blocking
|
|
11
|
+
- identity-guard
|
|
12
|
+
dependencies: []
|
|
13
|
+
interface: Claude Code Hook
|
|
14
|
+
requires:
|
|
15
|
+
binaries: [node]
|
|
16
|
+
openclaw:
|
|
17
|
+
emoji: "🛡️"
|
|
18
|
+
install:
|
|
19
|
+
env: []
|
|
20
|
+
author:
|
|
21
|
+
name: Parker Todd Brooks
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# wip-file-guard
|
|
25
|
+
|
|
26
|
+
Hook that blocks destructive edits to protected identity files. For Claude Code CLI and OpenClaw.
|
|
27
|
+
|
|
28
|
+
## When to Use This Skill
|
|
29
|
+
|
|
30
|
+
**Use wip-file-guard for:**
|
|
31
|
+
- Protecting CLAUDE.md, SOUL.md, IDENTITY.md, MEMORY.md, and other identity files from being overwritten
|
|
32
|
+
- Blocking AI agents from replacing file content instead of extending it
|
|
33
|
+
- Surviving context compaction (behavioral rules get erased, but hooks don't)
|
|
34
|
+
|
|
35
|
+
**This is a technical guardrail, not a prompt.** It blocks the operation before it happens.
|
|
36
|
+
|
|
37
|
+
### Do NOT Use For
|
|
38
|
+
|
|
39
|
+
- Protecting binary files or images
|
|
40
|
+
- Blocking all edits (it allows small edits, only blocks destructive ones)
|
|
41
|
+
- Repos without identity files
|
|
42
|
+
|
|
43
|
+
## How It Works
|
|
44
|
+
|
|
45
|
+
Two rules:
|
|
46
|
+
|
|
47
|
+
1. **Write is blocked** on protected files. Always. Use Edit instead.
|
|
48
|
+
2. **Edit is blocked** when it removes more than 2 net lines from a protected file.
|
|
49
|
+
|
|
50
|
+
### Protected Files
|
|
51
|
+
|
|
52
|
+
CLAUDE.md, SHARED-CONTEXT.md, SOUL.md, IDENTITY.md, CONTEXT.md, TOOLS.md, MEMORY.md
|
|
53
|
+
|
|
54
|
+
### Protected Patterns
|
|
55
|
+
|
|
56
|
+
Any file matching: memory, memories, journal, diary, daily log
|
|
57
|
+
|
|
58
|
+
## API Reference
|
|
59
|
+
|
|
60
|
+
### CLI
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
node guard.mjs --list # list protected files
|
|
64
|
+
bash test.sh # run test suite
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Claude Code Hook
|
|
68
|
+
|
|
69
|
+
Add to `~/.claude/settings.json`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"hooks": {
|
|
74
|
+
"PreToolUse": [
|
|
75
|
+
{
|
|
76
|
+
"matcher": "Edit|Write",
|
|
77
|
+
"hooks": [
|
|
78
|
+
{
|
|
79
|
+
"type": "command",
|
|
80
|
+
"command": "node \"/path/to/wip-file-guard/guard.mjs\"",
|
|
81
|
+
"timeout": 5
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Troubleshooting
|
|
91
|
+
|
|
92
|
+
### Agent keeps trying to Write
|
|
93
|
+
|
|
94
|
+
The deny message tells the agent to re-read the file and use Edit instead. If the agent ignores it, it's likely post-compaction and has lost context. The hook will keep blocking.
|
|
95
|
+
|
|
96
|
+
### Edit blocked unexpectedly
|
|
97
|
+
|
|
98
|
+
Check the net line removal. Edits that remove more than 2 lines from a protected file are blocked. Small edits (adding or replacing 1-2 lines) are allowed.
|
package/guard.mjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cc-file-guard/guard.mjs
|
|
3
|
+
// PreToolUse hook for Claude Code.
|
|
4
|
+
// Blocks destructive edits to protected files.
|
|
5
|
+
// - Blocks Write tool on protected files entirely
|
|
6
|
+
// - Blocks Edit when net line removal > 2 lines
|
|
7
|
+
|
|
8
|
+
import { basename } from 'node:path';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
|
|
11
|
+
// Exact basename matches
|
|
12
|
+
export const PROTECTED = new Set([
|
|
13
|
+
'CLAUDE.md',
|
|
14
|
+
'SHARED-CONTEXT.md',
|
|
15
|
+
'SOUL.md',
|
|
16
|
+
'IDENTITY.md',
|
|
17
|
+
'CONTEXT.md',
|
|
18
|
+
'TOOLS.md',
|
|
19
|
+
'MEMORY.md',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// Pattern matches (case-insensitive, checked against full path and basename)
|
|
23
|
+
export const PROTECTED_PATTERNS = [
|
|
24
|
+
/memory/i,
|
|
25
|
+
/memories/i,
|
|
26
|
+
/journal/i,
|
|
27
|
+
/diary/i,
|
|
28
|
+
/daily.*log/i,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function isProtected(filePath) {
|
|
32
|
+
const name = basename(filePath);
|
|
33
|
+
if (PROTECTED.has(name)) return name;
|
|
34
|
+
for (const pattern of PROTECTED_PATTERNS) {
|
|
35
|
+
if (pattern.test(filePath)) return name + ` (matched pattern: ${pattern})`;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function deny(reason) {
|
|
41
|
+
const output = {
|
|
42
|
+
hookSpecificOutput: {
|
|
43
|
+
hookEventName: 'PreToolUse',
|
|
44
|
+
permissionDecision: 'deny',
|
|
45
|
+
permissionDecisionReason: reason,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
process.stdout.write(JSON.stringify(output));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function countLines(str) {
|
|
52
|
+
if (!str) return 0;
|
|
53
|
+
return str.split('\n').length;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// CLI mode: node guard.mjs --list
|
|
57
|
+
if (process.argv.includes('--list')) {
|
|
58
|
+
console.log('Protected files (exact):');
|
|
59
|
+
for (const f of PROTECTED) console.log(` ${f}`);
|
|
60
|
+
console.log('Protected patterns:');
|
|
61
|
+
for (const p of PROTECTED_PATTERNS) console.log(` ${p}`);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main() {
|
|
66
|
+
let raw = '';
|
|
67
|
+
for await (const chunk of process.stdin) {
|
|
68
|
+
raw += chunk;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let input;
|
|
72
|
+
try {
|
|
73
|
+
input = JSON.parse(raw);
|
|
74
|
+
} catch {
|
|
75
|
+
// Can't parse input, allow by default
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const toolName = input.tool_name || '';
|
|
80
|
+
const toolInput = input.tool_input || {};
|
|
81
|
+
const filePath = toolInput.file_path || toolInput.filePath || '';
|
|
82
|
+
const fileName = basename(filePath);
|
|
83
|
+
|
|
84
|
+
// Only check protected files
|
|
85
|
+
const match = isProtected(filePath);
|
|
86
|
+
if (!match) {
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Block Write on protected files
|
|
91
|
+
// Exact matches: always block Write (use Edit instead)
|
|
92
|
+
// Pattern matches: only block if file already exists (allow creating new files)
|
|
93
|
+
if (toolName === 'Write') {
|
|
94
|
+
const isExactMatch = PROTECTED.has(fileName);
|
|
95
|
+
if (isExactMatch || existsSync(filePath)) {
|
|
96
|
+
deny(`BLOCKED: Write tool on ${match} is not allowed. Use Edit to make specific changes. Never overwrite protected files.`);
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
// Pattern match but file doesn't exist yet — allow creation
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// For Edit, check line removal AND large replacements
|
|
104
|
+
if (toolName === 'Edit') {
|
|
105
|
+
const oldString = toolInput.old_string || '';
|
|
106
|
+
const newString = toolInput.new_string || '';
|
|
107
|
+
const oldLines = countLines(oldString);
|
|
108
|
+
const newLines = countLines(newString);
|
|
109
|
+
const removed = oldLines - newLines;
|
|
110
|
+
|
|
111
|
+
// Block net removal of more than 2 lines
|
|
112
|
+
if (removed > 2) {
|
|
113
|
+
deny(`BLOCKED: You are removing ${removed} lines from ${match} (old: ${oldLines} lines, new: ${newLines} lines). Re-read the file and add content instead of replacing it.`);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Block large replacements (swapping big chunks even if line count is similar)
|
|
118
|
+
if (oldLines > 4 && oldString !== newString) {
|
|
119
|
+
deny(`BLOCKED: You are replacing ${oldLines} lines in ${match}. Edit smaller sections or append new content instead of replacing existing content.`);
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Allow
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main().catch(() => process.exit(0));
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wipcomputer/wip-file-guard",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Hook that blocks destructive edits to protected identity files. For Claude Code CLI and OpenClaw.",
|
|
6
|
+
"main": "guard.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"wip-file-guard": "./guard.mjs"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "bash test.sh"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude-code",
|
|
15
|
+
"openclaw",
|
|
16
|
+
"hook",
|
|
17
|
+
"file-guard",
|
|
18
|
+
"ai-safety",
|
|
19
|
+
"pretooluse"
|
|
20
|
+
],
|
|
21
|
+
"author": "Parker Todd Brooks",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/wipcomputer/wip-file-guard.git"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/test.sh
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# test.sh - Test wip-file-guard hook
|
|
3
|
+
# Run: bash test.sh
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
GUARD="$SCRIPT_DIR/guard.mjs"
|
|
7
|
+
PASS=0
|
|
8
|
+
FAIL=0
|
|
9
|
+
|
|
10
|
+
check() {
|
|
11
|
+
local desc="$1"
|
|
12
|
+
local input="$2"
|
|
13
|
+
local expect="$3" # "block" or "allow"
|
|
14
|
+
|
|
15
|
+
local output
|
|
16
|
+
output=$(echo "$input" | node "$GUARD" 2>/dev/null)
|
|
17
|
+
local code=$?
|
|
18
|
+
|
|
19
|
+
if [ "$expect" = "block" ]; then
|
|
20
|
+
if echo "$output" | grep -q "deny"; then
|
|
21
|
+
echo "PASS: $desc"
|
|
22
|
+
((PASS++))
|
|
23
|
+
else
|
|
24
|
+
echo "FAIL: $desc (expected block, got allow)"
|
|
25
|
+
((FAIL++))
|
|
26
|
+
fi
|
|
27
|
+
else
|
|
28
|
+
if [ -z "$output" ] && [ $code -eq 0 ]; then
|
|
29
|
+
echo "PASS: $desc"
|
|
30
|
+
((PASS++))
|
|
31
|
+
else
|
|
32
|
+
echo "FAIL: $desc (expected allow, got: $output)"
|
|
33
|
+
((FAIL++))
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
echo "wip-file-guard tests"
|
|
39
|
+
echo "==================="
|
|
40
|
+
echo ""
|
|
41
|
+
|
|
42
|
+
# Write tests
|
|
43
|
+
check "Block Write to CLAUDE.md" \
|
|
44
|
+
'{"tool_name":"Write","tool_input":{"file_path":"/foo/CLAUDE.md","content":"new"}}' \
|
|
45
|
+
"block"
|
|
46
|
+
|
|
47
|
+
check "Block Write to SHARED-CONTEXT.md" \
|
|
48
|
+
'{"tool_name":"Write","tool_input":{"file_path":"/bar/workspace/SHARED-CONTEXT.md","content":"new"}}' \
|
|
49
|
+
"block"
|
|
50
|
+
|
|
51
|
+
check "Allow Write to random file" \
|
|
52
|
+
'{"tool_name":"Write","tool_input":{"file_path":"/foo/bar.js","content":"new"}}' \
|
|
53
|
+
"allow"
|
|
54
|
+
|
|
55
|
+
# Edit tests - line removal
|
|
56
|
+
check "Block Edit removing 5 lines from CLAUDE.md" \
|
|
57
|
+
'{"tool_name":"Edit","tool_input":{"file_path":"/foo/CLAUDE.md","old_string":"a\nb\nc\nd\ne\nf","new_string":"replaced"}}' \
|
|
58
|
+
"block"
|
|
59
|
+
|
|
60
|
+
check "Allow Edit adding lines to CLAUDE.md" \
|
|
61
|
+
'{"tool_name":"Edit","tool_input":{"file_path":"/foo/CLAUDE.md","old_string":"a","new_string":"a\nb\nc"}}' \
|
|
62
|
+
"allow"
|
|
63
|
+
|
|
64
|
+
check "Allow Edit on non-protected file (even removing lines)" \
|
|
65
|
+
'{"tool_name":"Edit","tool_input":{"file_path":"/foo/bar.js","old_string":"a\nb\nc\nd\ne","new_string":"x"}}' \
|
|
66
|
+
"allow"
|
|
67
|
+
|
|
68
|
+
check "Allow Edit with small removal (2 lines)" \
|
|
69
|
+
'{"tool_name":"Edit","tool_input":{"file_path":"/foo/SOUL.md","old_string":"a\nb\nc","new_string":"x"}}' \
|
|
70
|
+
"allow"
|
|
71
|
+
|
|
72
|
+
check "Block Edit with 4 line removal from SOUL.md" \
|
|
73
|
+
'{"tool_name":"Edit","tool_input":{"file_path":"/foo/SOUL.md","old_string":"a\nb\nc\nd\ne\nf","new_string":"x\ny"}}' \
|
|
74
|
+
"block"
|
|
75
|
+
|
|
76
|
+
# Protected file coverage
|
|
77
|
+
check "Block Write to IDENTITY.md" \
|
|
78
|
+
'{"tool_name":"Write","tool_input":{"file_path":"/any/path/IDENTITY.md","content":"new"}}' \
|
|
79
|
+
"block"
|
|
80
|
+
|
|
81
|
+
check "Block Write to TOOLS.md" \
|
|
82
|
+
'{"tool_name":"Write","tool_input":{"file_path":"/any/path/TOOLS.md","content":"new"}}' \
|
|
83
|
+
"block"
|
|
84
|
+
|
|
85
|
+
# Large replacement (same line count, different content)
|
|
86
|
+
check "Block Edit replacing 8 lines with 8 different lines in SHARED-CONTEXT.md" \
|
|
87
|
+
'{"tool_name":"Edit","tool_input":{"file_path":"/foo/SHARED-CONTEXT.md","old_string":"line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8","new_string":"new1\nnew2\nnew3\nnew4\nnew5\nnew6\nnew7\nnew8"}}' \
|
|
88
|
+
"block"
|
|
89
|
+
|
|
90
|
+
check "Allow Edit replacing 3 lines in CLAUDE.md" \
|
|
91
|
+
'{"tool_name":"Edit","tool_input":{"file_path":"/foo/CLAUDE.md","old_string":"a\nb\nc","new_string":"x\ny\nz"}}' \
|
|
92
|
+
"allow"
|
|
93
|
+
|
|
94
|
+
# Pattern matching - Write
|
|
95
|
+
# Pattern-matched files: allow Write for NEW files, block for existing files
|
|
96
|
+
check "Allow Write to NEW file in memory/ directory (file doesn't exist)" \
|
|
97
|
+
'{"tool_name":"Write","tool_input":{"file_path":"/nonexistent/memory/2099-01-01.md","content":"new"}}' \
|
|
98
|
+
"allow"
|
|
99
|
+
|
|
100
|
+
check "Allow Write to NEW journal file (file doesn't exist)" \
|
|
101
|
+
'{"tool_name":"Write","tool_input":{"file_path":"/nonexistent/journals/new-entry.md","content":"new"}}' \
|
|
102
|
+
"allow"
|
|
103
|
+
|
|
104
|
+
# Pattern matching - Edit (existing files still protected from destructive edits)
|
|
105
|
+
check "Block Edit removing lines from daily log" \
|
|
106
|
+
'{"tool_name":"Edit","tool_input":{"file_path":"/memory/daily/2026-02-19.md","old_string":"a\nb\nc\nd\ne","new_string":"x"}}' \
|
|
107
|
+
"block"
|
|
108
|
+
|
|
109
|
+
# Block Write to pattern-matched file that EXISTS on disk
|
|
110
|
+
check "Block Write to existing test.sh (matched pattern: memory in path)" \
|
|
111
|
+
"{\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$SCRIPT_DIR/test.sh\",\"content\":\"new\"}}" \
|
|
112
|
+
"allow"
|
|
113
|
+
|
|
114
|
+
check "Allow Write to unrelated file with no pattern match" \
|
|
115
|
+
'{"tool_name":"Write","tool_input":{"file_path":"/src/utils/helper.js","content":"new"}}' \
|
|
116
|
+
"allow"
|
|
117
|
+
|
|
118
|
+
echo ""
|
|
119
|
+
echo "Results: $PASS passed, $FAIL failed"
|