claude-guardrails-rg 1.0.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/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # claude-guardrails
2
+
3
+ Automatic irreversibility guardrails for [Claude Code](https://claude.ai/code) — blocks destructive shell commands and dangerous file writes **before they execute**, on any OS.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g claude-guardrails
9
+ ```
10
+
11
+ The installer will ask where to apply the guardrails:
12
+
13
+ ```
14
+ šŸ›”ļø Claude Code Guardrails Installer
15
+
16
+ [G] Global — protects all projects (~/.claude/settings.json)
17
+ [L] Local — protects this project (./.claude/settings.json)
18
+
19
+ Where do you want to install? [G/l]:
20
+ ```
21
+
22
+ Press `Enter` to install globally (recommended), or type `l` for the current project only.
23
+
24
+ ## Uninstall
25
+
26
+ ```bash
27
+ npm uninstall -g claude-guardrails
28
+ ```
29
+
30
+ The uninstaller asks the same question and removes only the guardrail entries — all other hooks and settings are left untouched.
31
+
32
+ ---
33
+
34
+ ## What gets blocked
35
+
36
+ ### Bash commands
37
+
38
+ | Category | Blocked patterns |
39
+ |---|---|
40
+ | **Filesystem** | `rm -rf`, `dd if=`, redirect to block device (`> /dev/sdX`) |
41
+ | **SQL DDL** | `DROP TABLE`, `DROP DATABASE`, `DROP SCHEMA`, `DROP INDEX`, `TRUNCATE TABLE` |
42
+ | **Python ORM / CLI** | `drop_all()`, `drop_table()`, `flask drop`, `flask db downgrade`, `alembic downgrade`, `manage.py flush` |
43
+ | **Database CLIs** | `psql -c DROP`, `mysql -e DROP`, `mysqladmin drop`, `mongo --eval drop`, `redis-cli FLUSHALL/FLUSHDB` |
44
+ | **JS/TS DB CLIs** | `prisma migrate reset`, `sequelize db:drop`, `knex migrate:rollback --all`, `heroku pg:reset` |
45
+ | **Cloud / Infra** | `terraform destroy`, `aws s3 rm --recursive`, `aws s3 rb`, `gcloud delete --quiet`, `kubectl delete namespace/all` |
46
+ | **Git** | `git reset --hard`, `git push --force`, `git clean -f`, `git branch -D`, `git checkout --` |
47
+ | **Process** | `kill -9`, `pkill -9` |
48
+
49
+ ### File writes — `.py` `.js` `.ts` `.jsx` `.tsx` `.sh` `.sql`
50
+
51
+ | Category | Blocked patterns |
52
+ |---|---|
53
+ | **SQL DDL** | `DROP TABLE`, `DROP DATABASE`, `DROP SCHEMA`, `DROP INDEX`, `DROP VIEW`, `DROP SEQUENCE`, `TRUNCATE TABLE`, `DELETE FROM` without `WHERE` |
54
+ | **SQLAlchemy** | `drop_all()`, `metadata.drop_all()`, `drop_table()`, `__table__.drop()` |
55
+ | **Alembic** | `op.drop_table()`, `op.drop_column()`, `op.drop_index()`, `op.drop_constraint()` |
56
+ | **Sequelize / Knex** | `queryInterface.dropTable()`, `queryInterface.dropAllTables()`, `.dropTable()`, `.dropTableIfExists()`, `schema.dropTable()` |
57
+ | **TypeORM** | `.dropDatabase()`, `synchronize(true)` |
58
+ | **MongoDB / Mongoose** | `collection.drop()`, `Model.collection.drop()`, `db.dropDatabase()`, `mongoose.connection.dropDatabase()`, `mongoose.connection.dropCollection()` |
59
+ | **Prisma** | `$executeRaw` with DROP, `migrate reset` |
60
+ | **Filesystem** | `shutil.rmtree()`, `rm -rf`, `fs.rmSync({recursive:true})`, `fs.rmdirSync({recursive:true})`, `rimraf()` |
61
+ | **Git** | `git reset --hard`, `git push --force`, `git clean -f` |
62
+
63
+ ---
64
+
65
+ ## How it works
66
+
67
+ On install, two scanner scripts are copied into your `.claude/hooks/` directory and registered in `settings.json` as `PreToolUse` hooks:
68
+
69
+ ```json
70
+ {
71
+ "hooks": {
72
+ "PreToolUse": [
73
+ {
74
+ "matcher": "Write|Edit",
75
+ "hooks": [{ "type": "command", "command": "node ~/.claude/hooks/scan_file_content.mjs" }]
76
+ },
77
+ {
78
+ "matcher": "Bash",
79
+ "hooks": [{ "type": "command", "command": "node ~/.claude/hooks/scan_bash_command.mjs" }]
80
+ }
81
+ ]
82
+ }
83
+ }
84
+ ```
85
+
86
+ Before every `Bash`, `Write`, or `Edit` tool call, Claude Code pipes the tool input JSON to the relevant scanner via stdin. If a dangerous pattern matches, the scanner exits with code `2` — Claude Code treats this as a block and shows the error in the terminal. The AI cannot proceed until you manually intervene.
87
+
88
+ **Safe to re-run** — install is idempotent and merges with any hooks you already have. It will never duplicate or overwrite existing entries.
89
+
90
+ ---
91
+
92
+ ## Compatibility
93
+
94
+ - Node.js 18+
95
+ - macOS, Linux, Windows
96
+ - No third-party dependencies
97
+
98
+ ## Manual install (if npm scripts are disabled)
99
+
100
+ Some CI or locked-down environments block npm lifecycle scripts. Run the installer directly:
101
+
102
+ ```bash
103
+ node $(npm root -g)/claude-guardrails/bin/install.js
104
+ ```
package/bin/install.js ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import readline from 'readline';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ // Forward slashes required inside JSON command strings on all platforms
12
+ const toUnixPath = (p) => p.split(path.sep).join('/');
13
+
14
+ function ask(question) {
15
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
16
+ return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim().toLowerCase()); }));
17
+ }
18
+
19
+ function applyHooks(claudeDir, hooksDir, settingsFile) {
20
+ if (!fs.existsSync(hooksDir)) {
21
+ fs.mkdirSync(hooksDir, { recursive: true });
22
+ }
23
+
24
+ fs.copyFileSync(
25
+ path.join(__dirname, 'scan_bash_command.mjs'),
26
+ path.join(hooksDir, 'scan_bash_command.mjs')
27
+ );
28
+ fs.copyFileSync(
29
+ path.join(__dirname, 'scan_file_content.mjs'),
30
+ path.join(hooksDir, 'scan_file_content.mjs')
31
+ );
32
+
33
+ let settings = {};
34
+ if (fs.existsSync(settingsFile)) {
35
+ try {
36
+ settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
37
+ } catch (e) {
38
+ console.error(`āŒ ${settingsFile} exists but could not be parsed as JSON.`);
39
+ console.error(' Fix it manually, then re-run the installer.');
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ const scanFileCmd = `node "${toUnixPath(path.join(hooksDir, 'scan_file_content.mjs'))}"`;
45
+ const scanBashCmd = `node "${toUnixPath(path.join(hooksDir, 'scan_bash_command.mjs'))}"`;
46
+
47
+ const GUARDRAILS = [
48
+ { matcher: 'Write|Edit', hooks: [{ type: 'command', command: scanFileCmd }], _fp: 'scan_file_content.mjs' },
49
+ { matcher: 'Bash', hooks: [{ type: 'command', command: scanBashCmd }], _fp: 'scan_bash_command.mjs' },
50
+ ];
51
+
52
+ if (!settings.hooks) settings.hooks = {};
53
+ const existing = Array.isArray(settings.hooks.PreToolUse) ? settings.hooks.PreToolUse : [];
54
+
55
+ for (const g of GUARDRAILS) {
56
+ const present = existing.some(e =>
57
+ Array.isArray(e.hooks) &&
58
+ e.hooks.some(h => typeof h.command === 'string' && h.command.includes(g._fp))
59
+ );
60
+ if (!present) {
61
+ const { _fp, ...entry } = g;
62
+ existing.push(entry);
63
+ }
64
+ }
65
+ settings.hooks.PreToolUse = existing;
66
+
67
+ const tmp = settingsFile + '.tmp';
68
+ fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8');
69
+ try {
70
+ fs.renameSync(tmp, settingsFile);
71
+ } catch {
72
+ fs.copyFileSync(tmp, settingsFile);
73
+ fs.unlinkSync(tmp);
74
+ }
75
+ }
76
+
77
+ async function main() {
78
+ console.log('šŸ›”ļø Claude Code Guardrails Installer\n');
79
+ console.log(' [G] Global — protects all projects (~/.claude/settings.json)');
80
+ console.log(' [L] Local — protects this project (./.claude/settings.json)\n');
81
+
82
+ const answer = await ask('Where do you want to install? [G/l]: ');
83
+ const isLocal = answer === 'l' || answer === 'local';
84
+
85
+ const claudeDir = isLocal ? path.join(process.cwd(), '.claude') : path.join(os.homedir(), '.claude');
86
+ const hooksDir = path.join(claudeDir, 'hooks');
87
+ const settingsFile = path.join(claudeDir, 'settings.json');
88
+
89
+ const label = isLocal ? `local project (.claude/)` : `global (~/.claude/)`;
90
+
91
+ try {
92
+ applyHooks(claudeDir, hooksDir, settingsFile);
93
+ console.log(`\nāœ… Guardrails installed ${label}`);
94
+ console.log(` Settings: ${settingsFile}`);
95
+ console.log(` Hooks: ${hooksDir}`);
96
+ } catch (err) {
97
+ console.error('āŒ Failed to install guardrails:', err.message);
98
+ process.exit(1);
99
+ }
100
+ }
101
+
102
+ main();
@@ -0,0 +1,129 @@
1
+ import fs from 'fs';
2
+
3
+ // 1. Read the JSON input from Claude via stdin
4
+ let input = '';
5
+ try {
6
+ input = fs.readFileSync(0, 'utf-8');
7
+ } catch (e) {
8
+ process.exit(0);
9
+ }
10
+
11
+ // 2. Parse out the targeted bash command string
12
+ let command = '';
13
+ try {
14
+ const data = JSON.parse(input);
15
+ command = data.tool_input?.command || '';
16
+ } catch (e) {
17
+ process.exit(0);
18
+ }
19
+
20
+ let blockedReason = '';
21
+
22
+ // --- Filesystem ---
23
+ if (/rm\s+-[a-zA-Z]*rf?|rm\s+-[a-zA-Z]*f[a-zA-Z]*r/.test(command)) {
24
+ blockedReason = 'rm -rf — recursive force delete';
25
+ } else if (/\bdd\s+if=/.test(command)) {
26
+ blockedReason = 'dd — raw disk write/wipe';
27
+ } else if (/>\s*\/dev\/sd[a-z]/.test(command)) {
28
+ blockedReason = 'redirect to block device — disk overwrite';
29
+ }
30
+
31
+ // --- SQL DDL via shell ---
32
+ else if (/\bDROP\s+TABLE\b/i.test(command)) {
33
+ blockedReason = 'DROP TABLE via shell';
34
+ } else if (/\bDROP\s+DATABASE\b/i.test(command)) {
35
+ blockedReason = 'DROP DATABASE via shell';
36
+ } else if (/\bDROP\s+SCHEMA\b/i.test(command)) {
37
+ blockedReason = 'DROP SCHEMA via shell';
38
+ } else if (/\bDROP\s+INDEX\b/i.test(command)) {
39
+ blockedReason = 'DROP INDEX via shell';
40
+ } else if (/\bTRUNCATE\s+TABLE\b/i.test(command)) {
41
+ blockedReason = 'TRUNCATE TABLE via shell';
42
+ }
43
+
44
+ // --- Python ORM / DB CLI ---
45
+ else if (/python.*drop_all/i.test(command)) {
46
+ blockedReason = 'drop_all() via python shell';
47
+ } else if (/python.*drop_table/i.test(command)) {
48
+ blockedReason = 'drop_table() via python shell';
49
+ } else if (/flask\s+(db\s+)?drop/i.test(command)) {
50
+ blockedReason = 'flask drop command';
51
+ } else if (/flask\s+db\s+downgrade/i.test(command)) {
52
+ blockedReason = 'flask db downgrade — rolls back migrations destructively';
53
+ } else if (/alembic\s+downgrade/i.test(command)) {
54
+ blockedReason = 'alembic downgrade — rolls back migrations';
55
+ } else if (/django.*flush/i.test(command) || /manage\.py\s+flush/.test(command)) {
56
+ blockedReason = 'manage.py flush — wipes all data from the database';
57
+ } else if (/manage\.py\s+migrate.*--fake-initial/.test(command)) {
58
+ blockedReason = 'migrate --fake-initial — can lead to data loss';
59
+ }
60
+
61
+ // --- Database CLI tools ---
62
+ else if (/psql\b.*-c\s+['"]?\s*DROP/i.test(command)) {
63
+ blockedReason = 'psql -c DROP — executing DROP statement via psql';
64
+ } else if (/mysql\b.*-e\s+['"]?\s*DROP/i.test(command)) {
65
+ blockedReason = 'mysql -e DROP — executing DROP statement via mysql CLI';
66
+ } else if (/mysqladmin\s+drop/i.test(command)) {
67
+ blockedReason = 'mysqladmin drop — drops a MySQL database';
68
+ } else if (/mongo\b.*--eval.*drop/i.test(command)) {
69
+ blockedReason = 'mongo --eval drop — dropping via mongo shell';
70
+ } else if (/mongodrop|mongoexport.*--drop/i.test(command)) {
71
+ blockedReason = 'mongo drop flag — drops target before import';
72
+ } else if (/redis-cli\s+(FLUSHALL|FLUSHDB)/i.test(command)) {
73
+ blockedReason = 'redis-cli FLUSHALL/FLUSHDB — wipes Redis data';
74
+ } else if (/npx\s+prisma\s+migrate\s+reset/i.test(command)) {
75
+ blockedReason = 'prisma migrate reset — wipes and recreates the database';
76
+ } else if (/npx\s+sequelize.*db:drop/i.test(command)) {
77
+ blockedReason = 'sequelize db:drop — drops the database';
78
+ } else if (/knex\s+migrate:rollback.*--all/i.test(command)) {
79
+ blockedReason = 'knex migrate:rollback --all — rolls back all migrations';
80
+ } else if (/heroku\s+pg:reset/i.test(command)) {
81
+ blockedReason = 'heroku pg:reset — resets the Heroku Postgres database';
82
+ }
83
+
84
+ // --- Cloud / Infrastructure ---
85
+ else if (/terraform\s+destroy/i.test(command)) {
86
+ blockedReason = 'terraform destroy — destroys all managed infrastructure';
87
+ } else if (/aws\s+s3\s+rm\b.*--recursive/i.test(command)) {
88
+ blockedReason = 'aws s3 rm --recursive — bulk-deletes S3 objects';
89
+ } else if (/aws\s+s3\s+rb\b/i.test(command)) {
90
+ blockedReason = 'aws s3 rb — removes an S3 bucket';
91
+ } else if (/gcloud.*delete/i.test(command) && /--quiet|-q/.test(command)) {
92
+ blockedReason = 'gcloud delete --quiet — silently deletes a cloud resource';
93
+ } else if (/kubectl\s+delete\s+(namespace|all)\b/i.test(command)) {
94
+ blockedReason = 'kubectl delete namespace/all — deletes Kubernetes resources';
95
+ }
96
+
97
+ // --- Git ---
98
+ else if (/git.*reset.*--hard/.test(command)) {
99
+ blockedReason = 'git reset --hard — discards all uncommitted changes';
100
+ } else if (/git.*push.*--force|git.*push.*-f\b/.test(command)) {
101
+ blockedReason = 'git push --force — overwrites remote history';
102
+ } else if (/git.*clean.*-f/.test(command)) {
103
+ blockedReason = 'git clean -f — permanently deletes untracked files';
104
+ } else if (/git.*branch.*-D/.test(command)) {
105
+ blockedReason = 'git branch -D — force deletes a branch';
106
+ } else if (/git.*checkout.*--/.test(command)) {
107
+ blockedReason = 'git checkout -- — discards file changes';
108
+ }
109
+
110
+ // --- Process ---
111
+ else if (/kill\s+-9|pkill\s+-9/.test(command)) {
112
+ blockedReason = 'kill -9 — force kills a process';
113
+ }
114
+
115
+ // 3. Block and print error if triggered
116
+ if (blockedReason) {
117
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
118
+ console.error(`GUARDRAILS BLOCKED: ${blockedReason}`);
119
+ console.error('');
120
+ console.error(`Command: ${command}`);
121
+ console.error('');
122
+ console.error('This command is irreversible and cannot be run through');
123
+ console.error('Claude Code. You must execute it manually in a terminal');
124
+ console.error('to ensure full human intent and review.');
125
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
126
+ process.exit(2);
127
+ }
128
+
129
+ process.exit(0);
@@ -0,0 +1,153 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ // 1. Read the JSON input from Claude via stdin
5
+ let input = '';
6
+ try {
7
+ input = fs.readFileSync(0, 'utf-8');
8
+ } catch (e) {
9
+ process.exit(0);
10
+ }
11
+
12
+ // 2. Parse tool input details
13
+ let file = '';
14
+ let content = '';
15
+
16
+ try {
17
+ const data = JSON.parse(input);
18
+ const toolInput = data.tool_input || {};
19
+ file = toolInput.file_path || '';
20
+ const fileContent = toolInput.content || '';
21
+ const editString = toolInput.new_string || '';
22
+ content = fileContent + editString;
23
+ } catch (e) {
24
+ process.exit(0);
25
+ }
26
+
27
+ // 3. Only scan source/test files, skip lock files, binaries, etc.
28
+ const allowedExtensions = ['.py', '.js', '.ts', '.jsx', '.tsx', '.sh', '.sql'];
29
+ if (!allowedExtensions.includes(path.extname(file).toLowerCase())) {
30
+ process.exit(0);
31
+ }
32
+
33
+ let blockedReason = '';
34
+
35
+ // --- SQL DDL ---
36
+ if (/\bDROP\s+TABLE\b/i.test(content)) {
37
+ blockedReason = 'DROP TABLE — deletes a table and all its data';
38
+ } else if (/\bDROP\s+DATABASE\b/i.test(content)) {
39
+ blockedReason = 'DROP DATABASE — deletes the entire database';
40
+ } else if (/\bDROP\s+SCHEMA\b/i.test(content)) {
41
+ blockedReason = 'DROP SCHEMA — deletes a schema and everything in it';
42
+ } else if (/\bDROP\s+INDEX\b/i.test(content)) {
43
+ blockedReason = 'DROP INDEX — removes an index';
44
+ } else if (/\bDROP\s+VIEW\b/i.test(content)) {
45
+ blockedReason = 'DROP VIEW — removes a view';
46
+ } else if (/\bDROP\s+SEQUENCE\b/i.test(content)) {
47
+ blockedReason = 'DROP SEQUENCE — removes a sequence';
48
+ } else if (/\bTRUNCATE\s+TABLE\b/i.test(content)) {
49
+ blockedReason = 'TRUNCATE TABLE — deletes all rows permanently';
50
+ } else if (/\bDELETE\s+FROM\b/i.test(content) && !/\bWHERE\b/i.test(content)) {
51
+ blockedReason = 'DELETE FROM without WHERE — deletes all rows';
52
+ }
53
+
54
+ // --- Python ORM / SQLAlchemy ---
55
+ else if (/\bdrop_all\s*\(/i.test(content)) {
56
+ blockedReason = 'drop_all() — drops all ORM-managed tables';
57
+ } else if (/metadata\.drop_all/i.test(content)) {
58
+ blockedReason = 'metadata.drop_all() — drops all tables';
59
+ } else if (/\bdrop_table\s*\(/i.test(content)) {
60
+ blockedReason = 'drop_table() — drops a database table';
61
+ } else if (/__table__\.drop\s*\(/i.test(content)) {
62
+ blockedReason = '__table__.drop() — drops the SQLAlchemy model table';
63
+ }
64
+
65
+ // --- Alembic migrations ---
66
+ else if (/op\.drop_table\s*\(/i.test(content)) {
67
+ blockedReason = 'op.drop_table() — Alembic: drops a table in migration';
68
+ } else if (/op\.drop_column\s*\(/i.test(content)) {
69
+ blockedReason = 'op.drop_column() — Alembic: removes a column and its data';
70
+ } else if (/op\.drop_index\s*\(/i.test(content)) {
71
+ blockedReason = 'op.drop_index() — Alembic: removes an index';
72
+ } else if (/op\.drop_constraint\s*\(/i.test(content)) {
73
+ blockedReason = 'op.drop_constraint() — Alembic: removes a constraint';
74
+ }
75
+
76
+ // --- Sequelize / Knex (JS/TS) ---
77
+ else if (/queryInterface\.dropTable\s*\(/i.test(content)) {
78
+ blockedReason = 'queryInterface.dropTable() — Sequelize: drops a table';
79
+ } else if (/queryInterface\.dropAllTables\s*\(/i.test(content)) {
80
+ blockedReason = 'queryInterface.dropAllTables() — Sequelize: drops all tables';
81
+ } else if (/\.dropTable\s*\(/i.test(content)) {
82
+ blockedReason = 'dropTable() — drops a database table';
83
+ } else if (/\.dropTableIfExists\s*\(/i.test(content)) {
84
+ blockedReason = 'dropTableIfExists() — drops a database table';
85
+ } else if (/schema\.dropTable\s*\(/i.test(content)) {
86
+ blockedReason = 'schema.dropTable() — Knex: drops a table';
87
+ }
88
+
89
+ // --- TypeORM ---
90
+ else if (/\.dropDatabase\s*\(/i.test(content)) {
91
+ blockedReason = 'dropDatabase() — drops the entire database';
92
+ } else if (/connection\.synchronize\s*\(\s*true/i.test(content)) {
93
+ blockedReason = 'synchronize(true) — TypeORM: drops and recreates all tables';
94
+ }
95
+
96
+ // --- MongoDB / Mongoose ---
97
+ else if (/collection\.drop\s*\(/i.test(content)) {
98
+ blockedReason = 'collection.drop() — drops a MongoDB collection';
99
+ } else if (/Model\.collection\.drop\s*\(/i.test(content)) {
100
+ blockedReason = 'Model.collection.drop() — drops a Mongoose model collection';
101
+ } else if (/db\.dropDatabase\s*\(/i.test(content)) {
102
+ blockedReason = 'db.dropDatabase() — drops the entire MongoDB database';
103
+ } else if (/mongoose\.connection\.dropDatabase\s*\(/i.test(content)) {
104
+ blockedReason = 'mongoose.connection.dropDatabase() — drops the MongoDB database';
105
+ } else if (/mongoose\.connection\.dropCollection\s*\(/i.test(content)) {
106
+ blockedReason = 'mongoose.connection.dropCollection() — drops a MongoDB collection';
107
+ }
108
+
109
+ // --- Prisma ---
110
+ else if (/prisma\.\$executeRaw.*DROP/i.test(content)) {
111
+ blockedReason = 'prisma.$executeRaw with DROP — raw destructive SQL via Prisma';
112
+ } else if (/migrate\s+reset/i.test(content)) {
113
+ blockedReason = 'migrate reset — wipes and recreates the database';
114
+ }
115
+
116
+ // --- Filesystem ---
117
+ else if (/shutil\.rmtree\s*\(/i.test(content)) {
118
+ blockedReason = 'shutil.rmtree() — recursively deletes a directory';
119
+ } else if (/\brm\s+-[a-zA-Z]*rf?/.test(content)) {
120
+ blockedReason = 'rm -rf — recursively force-deletes files';
121
+ } else if (/fs\.rmSync\s*\(.*recursive\s*:\s*true/i.test(content)) {
122
+ blockedReason = 'fs.rmSync({ recursive: true }) — recursively deletes a directory';
123
+ } else if (/fs\.rmdirSync\s*\(.*recursive\s*:\s*true/i.test(content)) {
124
+ blockedReason = 'fs.rmdirSync({ recursive: true }) — recursively deletes a directory';
125
+ } else if (/rimraf\s*\(/i.test(content)) {
126
+ blockedReason = 'rimraf() — recursively deletes a directory';
127
+ }
128
+
129
+ // --- Git ---
130
+ else if (/git.*reset.*--hard/.test(content)) {
131
+ blockedReason = 'git reset --hard — discards all uncommitted changes';
132
+ } else if (/git.*push.*--force/.test(content)) {
133
+ blockedReason = 'git push --force — overwrites remote history';
134
+ } else if (/git.*clean.*-f/.test(content)) {
135
+ blockedReason = 'git clean -f — permanently deletes untracked files';
136
+ }
137
+
138
+ // 4. Block and print error if triggered
139
+ if (blockedReason) {
140
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
141
+ console.error(`GUARDRAILS BLOCKED: ${blockedReason}`);
142
+ console.error(`File: ${file}`);
143
+ console.error('');
144
+ console.error('This operation is irreversible and cannot be written');
145
+ console.error('by an AI agent. You must write this manually in your');
146
+ console.error('editor to ensure full human intent and review.');
147
+ console.error('');
148
+ console.error('If you truly need this, edit the file directly.');
149
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
150
+ process.exit(2);
151
+ }
152
+
153
+ process.exit(0);
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import readline from 'readline';
7
+
8
+ const GUARDRAIL_SCRIPTS = [
9
+ 'scan_bash_command.mjs',
10
+ 'scan_file_content.mjs',
11
+ ];
12
+
13
+ function ask(question) {
14
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
15
+ return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim().toLowerCase()); }));
16
+ }
17
+
18
+ async function main() {
19
+ console.log('šŸ—‘ļø Claude Code Guardrails Uninstaller\n');
20
+ console.log(' [G] Global — removes from all projects (~/.claude/settings.json)');
21
+ console.log(' [L] Local — removes from this project (./.claude/settings.json)\n');
22
+
23
+ const answer = await ask('Where do you want to uninstall from? [G/l]: ');
24
+ const isLocal = answer === 'l' || answer === 'local';
25
+
26
+ const claudeDir = isLocal ? path.join(process.cwd(), '.claude') : path.join(os.homedir(), '.claude');
27
+ const hooksDir = path.join(claudeDir, 'hooks');
28
+ const settingsFile = path.join(claudeDir, 'settings.json');
29
+
30
+ console.log('');
31
+
32
+ try {
33
+ // 1. Remove guardrail hook entries from settings.json
34
+ if (fs.existsSync(settingsFile)) {
35
+ let settings = {};
36
+ try {
37
+ settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8'));
38
+ } catch (e) {
39
+ console.warn('āš ļø settings.json could not be parsed. Skipping settings cleanup.');
40
+ }
41
+
42
+ if (settings.hooks && Array.isArray(settings.hooks.PreToolUse)) {
43
+ const before = settings.hooks.PreToolUse.length;
44
+
45
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(entry => {
46
+ if (!Array.isArray(entry.hooks)) return true;
47
+ return !entry.hooks.some(h =>
48
+ typeof h.command === 'string' &&
49
+ GUARDRAIL_SCRIPTS.some(s => h.command.includes(s))
50
+ );
51
+ });
52
+
53
+ const removed = before - settings.hooks.PreToolUse.length;
54
+
55
+ if (settings.hooks.PreToolUse.length === 0) delete settings.hooks.PreToolUse;
56
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
57
+
58
+ const tmp = settingsFile + '.tmp';
59
+ fs.writeFileSync(tmp, JSON.stringify(settings, null, 2), 'utf8');
60
+ try {
61
+ fs.renameSync(tmp, settingsFile);
62
+ } catch {
63
+ fs.copyFileSync(tmp, settingsFile);
64
+ fs.unlinkSync(tmp);
65
+ }
66
+
67
+ console.log(`Removed ${removed} guardrail hook(s) from ${settingsFile}`);
68
+ } else {
69
+ console.log('No guardrail hooks found in settings.json.');
70
+ }
71
+ } else {
72
+ console.log('settings.json not found. Nothing to clean up.');
73
+ }
74
+
75
+ // 2. Delete the scanner script files
76
+ for (const script of GUARDRAIL_SCRIPTS) {
77
+ const dest = path.join(hooksDir, script);
78
+ if (fs.existsSync(dest)) {
79
+ fs.unlinkSync(dest);
80
+ console.log(`Removed ${dest}`);
81
+ }
82
+ }
83
+
84
+ console.log('\nāœ… Uninstall complete. Other hooks and settings untouched.');
85
+
86
+ } catch (err) {
87
+ console.error('āŒ Failed to uninstall guardrails:', err.message);
88
+ process.exit(1);
89
+ }
90
+ }
91
+
92
+ main();
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "claude-guardrails-rg",
3
+ "version": "1.0.0",
4
+ "description": "Cross-platform global security guardrails for Claude Code",
5
+ "type": "module",
6
+ "main": "bin/install.js",
7
+ "scripts": {
8
+ "postinstall": "node bin/install.js",
9
+ "preuninstall": "node bin/uninstall.js"
10
+ },
11
+ "files": [
12
+ "bin/"
13
+ ],
14
+ "keywords": [
15
+ "claude",
16
+ "claude-code",
17
+ "guardrails",
18
+ "security"
19
+ ],
20
+ "author": "Mohd Rahban Ghani",
21
+ "license": "ISC"
22
+ }