ai-fs-permissions 0.1.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.
Files changed (58) hide show
  1. package/README.md +223 -0
  2. package/dist/checker.d.ts +6 -0
  3. package/dist/checker.d.ts.map +1 -0
  4. package/dist/checker.js +86 -0
  5. package/dist/checker.js.map +1 -0
  6. package/dist/cli-args.d.ts +25 -0
  7. package/dist/cli-args.d.ts.map +1 -0
  8. package/dist/cli-args.js +79 -0
  9. package/dist/cli-args.js.map +1 -0
  10. package/dist/cli-help.d.ts +2 -0
  11. package/dist/cli-help.d.ts.map +1 -0
  12. package/dist/cli-help.js +54 -0
  13. package/dist/cli-help.js.map +1 -0
  14. package/dist/cli.d.ts +9 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +83 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/config/loader.d.ts +22 -0
  19. package/dist/config/loader.d.ts.map +1 -0
  20. package/dist/config/loader.js +133 -0
  21. package/dist/config/loader.js.map +1 -0
  22. package/dist/config/schema.d.ts +102 -0
  23. package/dist/config/schema.d.ts.map +1 -0
  24. package/dist/config/schema.js +24 -0
  25. package/dist/config/schema.js.map +1 -0
  26. package/dist/exit-codes.d.ts +12 -0
  27. package/dist/exit-codes.d.ts.map +1 -0
  28. package/dist/exit-codes.js +12 -0
  29. package/dist/exit-codes.js.map +1 -0
  30. package/dist/index.d.ts +16 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +21 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/matcher/glob.d.ts +5 -0
  35. package/dist/matcher/glob.d.ts.map +1 -0
  36. package/dist/matcher/glob.js +18 -0
  37. package/dist/matcher/glob.js.map +1 -0
  38. package/dist/matcher/index.d.ts +8 -0
  39. package/dist/matcher/index.d.ts.map +1 -0
  40. package/dist/matcher/index.js +14 -0
  41. package/dist/matcher/index.js.map +1 -0
  42. package/dist/matcher/regex.d.ts +5 -0
  43. package/dist/matcher/regex.d.ts.map +1 -0
  44. package/dist/matcher/regex.js +16 -0
  45. package/dist/matcher/regex.js.map +1 -0
  46. package/dist/output.d.ts +18 -0
  47. package/dist/output.d.ts.map +1 -0
  48. package/dist/output.js +44 -0
  49. package/dist/output.js.map +1 -0
  50. package/dist/stdin.d.ts +21 -0
  51. package/dist/stdin.d.ts.map +1 -0
  52. package/dist/stdin.js +127 -0
  53. package/dist/stdin.js.map +1 -0
  54. package/dist/types.d.ts +60 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +12 -0
  57. package/dist/types.js.map +1 -0
  58. package/package.json +75 -0
package/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # ai-fs-permissions
2
+
3
+ A platform-agnostic security tool that enforces file system permissions for AI agents. Designed to protect sensitive files and folders from unauthorized access by AI coding assistants.
4
+
5
+ ## Why?
6
+
7
+ When using AI coding assistants like Claude Code, Gemini CLI, or Cursor, you might want to:
8
+ - Protect configuration folders (`.centy`, `.claude`, `.github`)
9
+ - Make certain files read-only (lock files, configs)
10
+ - Block access to sensitive files (`.env`, secrets, keys)
11
+ - Allow read but block write to specific paths
12
+
13
+ This package provides a CLI that can enforce these permissions, working with any AI tool that supports command hooks.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pnpm add -g ai-fs-permissions
19
+ ```
20
+
21
+ Or use without installation via `pnpm dlx ai-fs-permissions` or `npx ai-fs-permissions`.
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ # Check if writing to .centy is allowed
27
+ ai-fs-permissions --op write --path ".centy/config.json"
28
+ # Exit code: 2 (blocked if configured)
29
+
30
+ # Check if reading is allowed
31
+ ai-fs-permissions --op read --path "src/index.ts"
32
+ # Exit code: 0 (allowed)
33
+
34
+ # Show current configuration
35
+ ai-fs-permissions --show-config
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ Create `.ai-fs-permissions.yaml` in your project root:
41
+
42
+ ```yaml
43
+ version: 1
44
+
45
+ rules:
46
+ # Protect centy configuration (read-only)
47
+ - path: ".centy/**"
48
+ access: read
49
+ reason: "Configuration folder - modifications may break tooling"
50
+
51
+ # Protect AI agent settings
52
+ - path: ".claude/**"
53
+ access: read
54
+ reason: "AI agent settings - must be modified by user only"
55
+
56
+ # Block all access to env files
57
+ - path: "**/.env*"
58
+ access: none
59
+ reason: "Environment secrets - sensitive data"
60
+
61
+ # Block key files using regex
62
+ - path: "^.*\\.key$"
63
+ type: regex
64
+ access: none
65
+ reason: "Key files contain sensitive credentials"
66
+
67
+ # Allow specific file within protected folder (negation)
68
+ - path: "!.centy/user-config.yaml"
69
+ access: readwrite
70
+ reason: "User-editable config"
71
+ ```
72
+
73
+ ### Access Levels
74
+
75
+ | Level | Read | Write | Description |
76
+ |-------|------|-------|-------------|
77
+ | `none` | Blocked | Blocked | Fully blocked |
78
+ | `read` | Allowed | Blocked | Read-only |
79
+ | `write` | Blocked | Allowed | Write-only (rare) |
80
+ | `readwrite` | Allowed | Allowed | Full access |
81
+
82
+ ### Config Discovery
83
+
84
+ Configs are discovered gitignore-style and merged:
85
+
86
+ 1. `~/.config/ai-fs-permissions/config.yaml` (user defaults)
87
+ 2. Walk up from cwd to find `.ai-fs-permissions.yaml` (project)
88
+ 3. `./.ai-fs-permissions.yaml` (current directory, highest priority)
89
+
90
+ Set `extends: false` in project config to ignore user defaults.
91
+
92
+ ## Platform Integration
93
+
94
+ ### Claude Code
95
+
96
+ Add to your `.claude/settings.json`:
97
+
98
+ ```json
99
+ {
100
+ "hooks": {
101
+ "PreToolUse": [
102
+ {
103
+ "matcher": "Read|Glob|Grep",
104
+ "hooks": [
105
+ {
106
+ "type": "command",
107
+ "command": "npx ai-fs-permissions --op read"
108
+ }
109
+ ]
110
+ },
111
+ {
112
+ "matcher": "Edit|Write|NotebookEdit",
113
+ "hooks": [
114
+ {
115
+ "type": "command",
116
+ "command": "npx ai-fs-permissions --op write"
117
+ }
118
+ ]
119
+ }
120
+ ]
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### Gemini CLI
126
+
127
+ Add to your `.gemini/settings.json`:
128
+
129
+ ```json
130
+ {
131
+ "hooks": {
132
+ "BeforeTool": [
133
+ {
134
+ "matcher": "read_file",
135
+ "hooks": [
136
+ {
137
+ "type": "command",
138
+ "command": "npx ai-fs-permissions --op read"
139
+ }
140
+ ]
141
+ },
142
+ {
143
+ "matcher": "write_file|edit_file",
144
+ "hooks": [
145
+ {
146
+ "type": "command",
147
+ "command": "npx ai-fs-permissions --op write"
148
+ }
149
+ ]
150
+ }
151
+ ]
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### Cursor
157
+
158
+ Create `.cursor/hooks.json`:
159
+
160
+ ```json
161
+ {
162
+ "version": 1,
163
+ "hooks": {
164
+ "beforeShellExecution": [
165
+ {
166
+ "command": "npx ai-fs-permissions --op write"
167
+ }
168
+ ]
169
+ }
170
+ }
171
+ ```
172
+
173
+ ## CLI Options
174
+
175
+ ```
176
+ ai-fs-permissions [options] [path]
177
+
178
+ Options:
179
+ --op <type> Operation to check: read, write (required)
180
+ --path <path> File path to check
181
+ --config <path> Use specific config file (skip discovery)
182
+ --show-config Show merged configuration and exit
183
+ --validate Validate config file and exit
184
+ --help, -h Show help message
185
+ --version, -v Show version
186
+
187
+ Input Methods:
188
+ 1. Command line: ai-fs-permissions --op write --path ".centy/config.json"
189
+ 2. Stdin (JSON): echo '{"tool_input":{"file_path":"..."}}' | ai-fs-permissions --op write
190
+ 3. Stdin (plain): echo ".centy/config.json" | ai-fs-permissions --op write
191
+ ```
192
+
193
+ ## Exit Codes
194
+
195
+ - `0` - Operation allowed
196
+ - `2` - Operation blocked
197
+ - `1` - Error occurred
198
+
199
+ ## Blocked Message
200
+
201
+ When an operation is blocked, a clear message is output to guide the AI agent:
202
+
203
+ ```
204
+ PERMISSION DENIED: Writing to '.centy/settings.json' is blocked.
205
+
206
+ Reason: Configuration folder - modifications may break tooling
207
+
208
+ This rule is configured by the project owner and cannot be overridden.
209
+ Do NOT attempt to:
210
+ - Use Bash commands to bypass this restriction
211
+ - Suggest workarounds that modify this path
212
+ - Ask the user to disable this protection
213
+
214
+ If this file must be modified, ask the user to do it manually.
215
+ ```
216
+
217
+ ## License
218
+
219
+ MIT
220
+
221
+ ## Related Projects
222
+
223
+ - [block-no-verify](https://github.com/tupe12334/block-no-verify) - Block `--no-verify` flag in git commands
@@ -0,0 +1,6 @@
1
+ import type { Config, Operation, CheckResult } from './types.js';
2
+ /**
3
+ * Checks if an operation on a path is allowed
4
+ */
5
+ export declare function checkPermission(config: Config, filePath: string, operation: Operation): CheckResult;
6
+ //# sourceMappingURL=checker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checker.d.ts","sourceRoot":"","sources":["../src/checker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EAEN,SAAS,EAET,WAAW,EACZ,MAAM,YAAY,CAAA;AA8CnB;;GAEG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,GACnB,WAAW,CAsDb"}
@@ -0,0 +1,86 @@
1
+ import { matchPath } from './matcher/index.js';
2
+ import { formatBlockedMessage, formatAllowedMessage } from './output.js';
3
+ /**
4
+ * Checks if an access level allows the given operation
5
+ */
6
+ function isOperationAllowed(access, operation) {
7
+ switch (access) {
8
+ case 'none':
9
+ return false;
10
+ case 'read':
11
+ return operation === 'read';
12
+ case 'write':
13
+ return operation === 'write';
14
+ case 'readwrite':
15
+ return true;
16
+ default:
17
+ return true;
18
+ }
19
+ }
20
+ /**
21
+ * Finds the matching rule for a path
22
+ * Rules are evaluated in order, with later rules overriding earlier ones
23
+ * Negation rules (!) grant access back
24
+ */
25
+ function findMatchingRule(rules, filePath) {
26
+ let lastMatch = null;
27
+ for (let i = 0; i < rules.length; i++) {
28
+ const rule = rules[i];
29
+ if (matchPath(rule.path, rule.type, filePath)) {
30
+ lastMatch = { rule, index: i };
31
+ }
32
+ }
33
+ return lastMatch;
34
+ }
35
+ /**
36
+ * Checks if an operation on a path is allowed
37
+ */
38
+ export function checkPermission(config, filePath, operation) {
39
+ // Normalize path (remove leading ./ if present)
40
+ const normalizedPath = filePath.replace(/^\.\//, '');
41
+ // Find matching rule
42
+ const match = findMatchingRule(config.rules, normalizedPath);
43
+ // No matching rule = passthrough (allowed)
44
+ if (match === null) {
45
+ return {
46
+ allowed: true,
47
+ path: normalizedPath,
48
+ operation,
49
+ message: formatAllowedMessage(normalizedPath, operation),
50
+ };
51
+ }
52
+ const { rule } = match;
53
+ // Negation rules grant access back
54
+ if (rule.negated) {
55
+ return {
56
+ allowed: true,
57
+ path: normalizedPath,
58
+ operation,
59
+ rule: `!${rule.path}`,
60
+ access: rule.access,
61
+ message: formatAllowedMessage(normalizedPath, operation),
62
+ };
63
+ }
64
+ // Check if operation is allowed by the access level
65
+ const allowed = isOperationAllowed(rule.access, operation);
66
+ if (allowed) {
67
+ return {
68
+ allowed: true,
69
+ path: normalizedPath,
70
+ operation,
71
+ rule: rule.path,
72
+ access: rule.access,
73
+ message: formatAllowedMessage(normalizedPath, operation),
74
+ };
75
+ }
76
+ // Operation is blocked
77
+ return {
78
+ allowed: false,
79
+ path: normalizedPath,
80
+ operation,
81
+ rule: rule.path,
82
+ access: rule.access,
83
+ message: formatBlockedMessage(normalizedPath, operation, rule),
84
+ };
85
+ }
86
+ //# sourceMappingURL=checker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checker.js","sourceRoot":"","sources":["../src/checker.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAExE;;GAEG;AACH,SAAS,kBAAkB,CACzB,MAAmB,EACnB,SAAoB;IAEpB,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,MAAM;YACT,OAAO,KAAK,CAAA;QACd,KAAK,MAAM;YACT,OAAO,SAAS,KAAK,MAAM,CAAA;QAC7B,KAAK,OAAO;YACV,OAAO,SAAS,KAAK,OAAO,CAAA;QAC9B,KAAK,WAAW;YACd,OAAO,IAAI,CAAA;QACb;YACE,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CACvB,KAAa,EACb,QAAgB;IAEhB,IAAI,SAAS,GAAyC,IAAI,CAAA;IAE1D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QACrB,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;YAC9C,SAAS,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAA;QAChC,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAc,EACd,QAAgB,EAChB,SAAoB;IAEpB,gDAAgD;IAChD,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IAEpD,qBAAqB;IACrB,MAAM,KAAK,GAAG,gBAAgB,CAAC,MAAM,CAAC,KAAK,EAAE,cAAc,CAAC,CAAA;IAE5D,2CAA2C;IAC3C,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO;YACL,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,cAAc;YACpB,SAAS;YACT,OAAO,EAAE,oBAAoB,CAAC,cAAc,EAAE,SAAS,CAAC;SACzD,CAAA;IACH,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAA;IAEtB,mCAAmC;IACnC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO;YACL,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,cAAc;YACpB,SAAS;YACT,IAAI,EAAE,IAAI,IAAI,CAAC,IAAI,EAAE;YACrB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,OAAO,EAAE,oBAAoB,CAAC,cAAc,EAAE,SAAS,CAAC;SACzD,CAAA;IACH,CAAC;IAED,oDAAoD;IACpD,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAE1D,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO;YACL,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,cAAc;YACpB,SAAS;YACT,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,OAAO,EAAE,oBAAoB,CAAC,cAAc,EAAE,SAAS,CAAC;SACzD,CAAA;IACH,CAAC;IAED,uBAAuB;IACvB,OAAO;QACL,OAAO,EAAE,KAAK;QACd,IAAI,EAAE,cAAc;QACpB,SAAS;QACT,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,oBAAoB,CAAC,cAAc,EAAE,SAAS,EAAE,IAAI,CAAC;KAC/D,CAAA;AACH,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { Operation } from './types.js';
2
+ /**
3
+ * Parsed CLI arguments
4
+ */
5
+ export interface CliArgs {
6
+ /** Operation to check (read/write) */
7
+ operation: Operation | null;
8
+ /** File path to check */
9
+ path: string | null;
10
+ /** Custom config file path */
11
+ configPath: string | null;
12
+ /** Show help */
13
+ showHelp: boolean;
14
+ /** Show version */
15
+ showVersion: boolean;
16
+ /** Show merged config (debug) */
17
+ showConfig: boolean;
18
+ /** Validate config file */
19
+ validate: boolean;
20
+ }
21
+ /**
22
+ * Parses command line arguments
23
+ */
24
+ export declare function parseArgs(args: string[], onError: (message: string) => never): CliArgs;
25
+ //# sourceMappingURL=cli-args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-args.d.ts","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,sCAAsC;IACtC,SAAS,EAAE,SAAS,GAAG,IAAI,CAAA;IAC3B,yBAAyB;IACzB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,8BAA8B;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,gBAAgB;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,mBAAmB;IACnB,WAAW,EAAE,OAAO,CAAA;IACpB,iCAAiC;IACjC,UAAU,EAAE,OAAO,CAAA;IACnB,2BAA2B;IAC3B,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,KAAK,GAClC,OAAO,CAqFT"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Parses command line arguments
3
+ */
4
+ export function parseArgs(args, onError) {
5
+ const result = {
6
+ operation: null,
7
+ path: null,
8
+ configPath: null,
9
+ showHelp: false,
10
+ showVersion: false,
11
+ showConfig: false,
12
+ validate: false,
13
+ };
14
+ let i = 0;
15
+ while (i < args.length) {
16
+ const arg = args[i];
17
+ if (arg === '--help' || arg === '-h') {
18
+ result.showHelp = true;
19
+ i++;
20
+ continue;
21
+ }
22
+ if (arg === '--version' || arg === '-v') {
23
+ result.showVersion = true;
24
+ i++;
25
+ continue;
26
+ }
27
+ if (arg === '--show-config') {
28
+ result.showConfig = true;
29
+ i++;
30
+ continue;
31
+ }
32
+ if (arg === '--validate') {
33
+ result.validate = true;
34
+ i++;
35
+ continue;
36
+ }
37
+ if (arg === '--op') {
38
+ const value = args[i + 1];
39
+ if (!value || value.startsWith('-')) {
40
+ onError('--op requires a value (read or write)');
41
+ }
42
+ if (value !== 'read' && value !== 'write') {
43
+ onError(`Invalid operation: ${value}. Must be 'read' or 'write'`);
44
+ }
45
+ result.operation = value;
46
+ i += 2;
47
+ continue;
48
+ }
49
+ if (arg === '--path') {
50
+ const value = args[i + 1];
51
+ if (!value || value.startsWith('-')) {
52
+ onError('--path requires a value');
53
+ }
54
+ result.path = value;
55
+ i += 2;
56
+ continue;
57
+ }
58
+ if (arg === '--config') {
59
+ const value = args[i + 1];
60
+ if (!value || value.startsWith('-')) {
61
+ onError('--config requires a value');
62
+ }
63
+ result.configPath = value;
64
+ i += 2;
65
+ continue;
66
+ }
67
+ // Unknown argument
68
+ if (arg.startsWith('-')) {
69
+ onError(`Unknown option: ${arg}`);
70
+ }
71
+ // Positional argument (treat as path if not set)
72
+ if (result.path === null) {
73
+ result.path = arg;
74
+ }
75
+ i++;
76
+ }
77
+ return result;
78
+ }
79
+ //# sourceMappingURL=cli-args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-args.js","sourceRoot":"","sources":["../src/cli-args.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const HELP_TEXT = "ai-fs-permissions - File system permissions for AI agents\n\nUSAGE\n ai-fs-permissions [options] [path]\n\nOPTIONS\n --op <type> Operation to check: read, write (required unless using stdin)\n --path <path> File path to check (can also be positional argument)\n --config <path> Use specific config file (skip discovery)\n --show-config Show merged configuration and exit\n --validate Validate config file and exit\n --help, -h Show this help message\n --version, -v Show version\n\nINPUT METHODS\n 1. Command line: ai-fs-permissions --op write --path \".centy/config.json\"\n 2. Stdin (JSON): echo '{\"tool_input\":{\"file_path\":\".centy/x\"}}' | ai-fs-permissions --op write\n 3. Stdin (plain): echo \".centy/config.json\" | ai-fs-permissions --op write\n\nCONFIG FILE (.ai-fs-permissions.yaml)\n version: 1\n rules:\n - path: \".centy/**\"\n access: read\n reason: \"Configuration folder\"\n - path: \"**/.env*\"\n access: none\n reason: \"Environment secrets\"\n\nACCESS LEVELS\n none Block all access (read and write)\n read Allow read, block write\n write Allow write, block read\n readwrite Allow both read and write\n\nEXIT CODES\n 0 Operation allowed\n 2 Operation blocked\n 1 Error (invalid config, missing arguments, etc.)\n\nEXAMPLES\n # Check if writing to .centy is allowed\n ai-fs-permissions --op write --path \".centy/settings.json\"\n\n # Use with Claude Code hooks (stdin JSON)\n echo '{\"tool_input\":{\"file_path\":\"src/index.ts\"}}' | ai-fs-permissions --op write\n\n # Show merged config for debugging\n ai-fs-permissions --show-config\n\n # Validate config file\n ai-fs-permissions --validate\n";
2
+ //# sourceMappingURL=cli-help.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-help.d.ts","sourceRoot":"","sources":["../src/cli-help.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,quDAoDrB,CAAA"}
@@ -0,0 +1,54 @@
1
+ export const HELP_TEXT = `ai-fs-permissions - File system permissions for AI agents
2
+
3
+ USAGE
4
+ ai-fs-permissions [options] [path]
5
+
6
+ OPTIONS
7
+ --op <type> Operation to check: read, write (required unless using stdin)
8
+ --path <path> File path to check (can also be positional argument)
9
+ --config <path> Use specific config file (skip discovery)
10
+ --show-config Show merged configuration and exit
11
+ --validate Validate config file and exit
12
+ --help, -h Show this help message
13
+ --version, -v Show version
14
+
15
+ INPUT METHODS
16
+ 1. Command line: ai-fs-permissions --op write --path ".centy/config.json"
17
+ 2. Stdin (JSON): echo '{"tool_input":{"file_path":".centy/x"}}' | ai-fs-permissions --op write
18
+ 3. Stdin (plain): echo ".centy/config.json" | ai-fs-permissions --op write
19
+
20
+ CONFIG FILE (.ai-fs-permissions.yaml)
21
+ version: 1
22
+ rules:
23
+ - path: ".centy/**"
24
+ access: read
25
+ reason: "Configuration folder"
26
+ - path: "**/.env*"
27
+ access: none
28
+ reason: "Environment secrets"
29
+
30
+ ACCESS LEVELS
31
+ none Block all access (read and write)
32
+ read Allow read, block write
33
+ write Allow write, block read
34
+ readwrite Allow both read and write
35
+
36
+ EXIT CODES
37
+ 0 Operation allowed
38
+ 2 Operation blocked
39
+ 1 Error (invalid config, missing arguments, etc.)
40
+
41
+ EXAMPLES
42
+ # Check if writing to .centy is allowed
43
+ ai-fs-permissions --op write --path ".centy/settings.json"
44
+
45
+ # Use with Claude Code hooks (stdin JSON)
46
+ echo '{"tool_input":{"file_path":"src/index.ts"}}' | ai-fs-permissions --op write
47
+
48
+ # Show merged config for debugging
49
+ ai-fs-permissions --show-config
50
+
51
+ # Validate config file
52
+ ai-fs-permissions --validate
53
+ `;
54
+ //# sourceMappingURL=cli-help.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-help.js","sourceRoot":"","sources":["../src/cli-help.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDxB,CAAA"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ai-fs-permissions CLI
4
+ *
5
+ * A platform-agnostic tool to enforce file system permissions for AI agents.
6
+ * Works with Claude Code, Gemini CLI, Cursor, and other AI coding tools.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;GAKG"}
package/dist/cli.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ai-fs-permissions CLI
4
+ *
5
+ * A platform-agnostic tool to enforce file system permissions for AI agents.
6
+ * Works with Claude Code, Gemini CLI, Cursor, and other AI coding tools.
7
+ */
8
+ import { parseArgs } from './cli-args.js';
9
+ import { HELP_TEXT } from './cli-help.js';
10
+ import { loadConfig } from './config/loader.js';
11
+ import { checkPermission } from './checker.js';
12
+ import { outputResult } from './output.js';
13
+ import { parseStdin, readStdin } from './stdin.js';
14
+ import { EXIT_CODES } from './exit-codes.js';
15
+ const VERSION = '0.1.0';
16
+ function handleError(message) {
17
+ console.error(`Error: ${message}`);
18
+ console.error('Use --help for usage information');
19
+ process.exit(EXIT_CODES.ERROR);
20
+ }
21
+ async function main() {
22
+ try {
23
+ const args = parseArgs(process.argv.slice(2), handleError);
24
+ if (args.showHelp) {
25
+ console.log(HELP_TEXT);
26
+ process.exit(EXIT_CODES.ALLOWED);
27
+ }
28
+ if (args.showVersion) {
29
+ console.log(VERSION);
30
+ process.exit(EXIT_CODES.ALLOWED);
31
+ }
32
+ const cwd = process.cwd();
33
+ const config = loadConfig(cwd, args.configPath ?? undefined);
34
+ if (args.showConfig) {
35
+ console.log(JSON.stringify(config, null, 2));
36
+ process.exit(EXIT_CODES.ALLOWED);
37
+ }
38
+ if (args.validate) {
39
+ if (config.rules.length === 0) {
40
+ console.log('No config file found or config has no rules');
41
+ }
42
+ else {
43
+ console.log(`Config valid: ${config.rules.length} rule(s) loaded`);
44
+ }
45
+ process.exit(EXIT_CODES.ALLOWED);
46
+ }
47
+ // Get path and operation from args or stdin
48
+ let filePath = args.path;
49
+ let operation = args.operation;
50
+ // If no path from args, try stdin
51
+ if (filePath === null) {
52
+ const stdinInput = await readStdin();
53
+ if (stdinInput.trim()) {
54
+ const parsed = parseStdin(stdinInput);
55
+ filePath = parsed.path;
56
+ // Infer operation from tool if not provided
57
+ if (operation === null && parsed.inferredOperation) {
58
+ operation = parsed.inferredOperation;
59
+ }
60
+ }
61
+ }
62
+ // Validate required arguments
63
+ if (filePath === null || filePath.trim() === '') {
64
+ // No path provided and no stdin - this is OK, just allow
65
+ // This handles cases where hooks are called but no file is involved
66
+ process.exit(EXIT_CODES.ALLOWED);
67
+ }
68
+ if (operation === null) {
69
+ handleError('Operation required. Use --op read or --op write');
70
+ }
71
+ // Check permission
72
+ const result = checkPermission(config, filePath, operation);
73
+ outputResult(result);
74
+ process.exit(result.allowed ? EXIT_CODES.ALLOWED : EXIT_CODES.BLOCKED);
75
+ }
76
+ catch (error) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ console.error('Error:', message);
79
+ process.exit(EXIT_CODES.ERROR);
80
+ }
81
+ }
82
+ main();
83
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { Config } from '../types.js';
2
+ /**
3
+ * Loads a config file from a path
4
+ */
5
+ export declare function loadConfigFile(filePath: string): Config | null;
6
+ /**
7
+ * Finds config file by walking up the directory tree (gitignore-style)
8
+ */
9
+ export declare function findConfigFile(startDir: string): string | null;
10
+ /**
11
+ * Gets the user config file path
12
+ */
13
+ export declare function getUserConfigPath(): string | null;
14
+ /**
15
+ * Merges multiple configs (later configs override earlier ones)
16
+ */
17
+ export declare function mergeConfigs(...configs: (Config | null)[]): Config;
18
+ /**
19
+ * Loads and merges all applicable configs
20
+ */
21
+ export declare function loadConfig(cwd: string, explicitPath?: string): Config;
22
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAqB,MAAM,aAAa,CAAA;AAuC5D;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuB9D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAe9D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAejD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,OAAO,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,MAAM,CAgBlE;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAwBrE"}