@hiro-c/agent-gate 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -158
- package/package.json +1 -1
- package/dist/collector/collectClaudeMd.d.ts +0 -3
- package/dist/collector/collectClaudeMd.d.ts.map +0 -1
- package/dist/collector/collectClaudeMd.js +0 -87
- package/dist/contracts/types/ClaudeMdFile.d.ts +0 -5
- package/dist/contracts/types/ClaudeMdFile.d.ts.map +0 -1
- package/dist/contracts/types/ClaudeMdFile.js +0 -2
package/README.md
CHANGED
|
@@ -1,204 +1,100 @@
|
|
|
1
1
|
# agent-gate
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Hiro-Chiba/agent-gate/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@hiro-c/agent-gate)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
**Runtime rule enforcer for AI coding agents.** Reads your existing instruction files (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, ...) and enforces them at hook time in Claude Code and Cursor.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## What it does
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
- **Stops catastrophic operations** before AI ever sees them: `rm -rf /`, writes to `.env` / `.ssh/*`, force-push to `main`, edits to `/etc`.
|
|
11
|
+
- **Aggregates 8 instruction file formats** into one rule set the AI uses to decide on the remaining cases.
|
|
12
|
+
- **Returns guidance, not denials**: block reasons describe the next correct step the agent should take.
|
|
13
|
+
- **Audits your rule files** with `agent-gate lint` (detects vague rules and ambiguous modifiers like "適切に" / "where possible").
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
## Install
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## Features
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @hiro-c/agent-gate
|
|
19
|
+
agent-gate install
|
|
20
|
+
```
|
|
19
21
|
|
|
20
|
-
-
|
|
21
|
-
- Adapter pattern: same binary works in Claude Code (`--agent claude-code`, default) and Cursor 1.7 (`--agent cursor`).
|
|
22
|
-
- Deterministic safety baseline with five built-in rules that fire before any AI call.
|
|
23
|
-
- AI validation against the combined rule set when the safety baseline passes.
|
|
24
|
-
- Per-rule disable and per-project customization via `.agent-gate.json` and `AGENT_GATE_DISABLED_RULES`.
|
|
25
|
-
- Optional decision log (`AGENT_GATE_LOG=1`) and `agent-gate stats` for auditing.
|
|
26
|
-
- Block reasons are guidance, not denials: the AI is instructed to describe the next correct step alongside the violated rule.
|
|
22
|
+
`agent-gate install` registers the Claude Code PreToolUse hook in `~/.claude/settings.json`. Restart Claude Code to activate. Use `agent-gate uninstall` to remove.
|
|
27
23
|
|
|
28
|
-
|
|
24
|
+
For Cursor 1.7, point your hook config at `agent-gate --agent cursor`.
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
- Claude Code or Cursor 1.7
|
|
26
|
+
## How it works
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
Every `Edit` / `Write` / `Bash` call from the agent goes through:
|
|
34
29
|
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
./install.sh
|
|
30
|
+
```
|
|
31
|
+
hook payload → adapter → deterministic rules → AI validation → verdict
|
|
32
|
+
(5 built-ins) (your rules)
|
|
39
33
|
```
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
To register against Cursor instead of Claude Code, run the binary with `--agent cursor` from your Cursor hook config. To remove the Claude Code hook, run `./uninstall.sh`.
|
|
44
|
-
|
|
45
|
-
## Configuration
|
|
46
|
-
|
|
47
|
-
### Environment variables
|
|
35
|
+
If a deterministic rule fires, AI is skipped. Otherwise agent-gate reads all instruction files in the project tree and asks the AI to validate the operation against them.
|
|
48
36
|
|
|
49
|
-
|
|
50
|
-
|---|---|---|
|
|
51
|
-
| `AGENT_GATE_MODEL` | `claude-sonnet-4-6` | Model used for AI validation |
|
|
52
|
-
| `AGENT_GATE_API_KEY` | (none) | Anthropic API key. Uses the API directly when set; otherwise spawns the Claude CLI |
|
|
53
|
-
| `AGENT_GATE_COOLDOWN` | `0` | Cooldown in seconds between AI validations (deterministic rules always fire) |
|
|
54
|
-
| `AGENT_GATE_DISABLED` | `false` | Set to `true` to disable the whole tool |
|
|
55
|
-
| `AGENT_GATE_DISABLED_RULES` | (none) | Comma-separated rule ids to disable, merged with the config file |
|
|
56
|
-
| `AGENT_GATE_LOG` | (none) | Set to `1` to append decisions to `~/.agent-gate/log.jsonl` |
|
|
57
|
-
| `AGENT_GATE_REASON_LANG` | `auto` | Language for AI-generated `reason` text. `auto` matches the instruction files (English fallback when mixed). Pass `en`, `ja`, `zh`, `ko`, etc. to force a specific language |
|
|
58
|
-
| `USE_SYSTEM_CLAUDE` | `false` | `true` forces PATH `claude` (default: `~/.claude/local/claude` with PATH fallback) |
|
|
37
|
+
## Built-in safety rules
|
|
59
38
|
|
|
60
|
-
|
|
39
|
+
Run by default, disable per-rule in `.agent-gate.json` if needed.
|
|
61
40
|
|
|
62
|
-
|
|
41
|
+
| Rule | Blocks |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `prevent-rm-rf-root` | `rm -rf` on `/`, `$HOME`, `~`, `/etc`, `/usr`, `/var`, etc. (handles `sudo`, flag variants). |
|
|
44
|
+
| `prevent-secret-file-write` | `Edit`/`Write` to `.env*`, `.ssh/*`, `.aws/credentials`, `*.pem`, `*.key`, `id_rsa`. |
|
|
45
|
+
| `prevent-bash-secret-write` | Shell redirects to the same paths (`echo > .env`, `tee .ssh/id_rsa`). |
|
|
46
|
+
| `prevent-force-push-main` | `git push --force` to `main`, `master`, `develop`, `release`, etc. Allows `--force-with-lease`. |
|
|
47
|
+
| `prevent-system-path-write` | `Edit`/`Write` to `/etc`, `/usr`, `/System`, `/Library`. |
|
|
63
48
|
|
|
64
|
-
|
|
49
|
+
## Config
|
|
65
50
|
|
|
66
|
-
|
|
51
|
+
Drop a `.agent-gate.config.ts` (or `.js` / `.json`) at the project root:
|
|
67
52
|
|
|
68
53
|
```ts
|
|
69
|
-
|
|
70
|
-
import { defineConfig, forbidCommandPattern, forbidFilePathPattern } from 'agent-gate'
|
|
54
|
+
import { defineConfig, forbidCommandPattern } from '@hiro-c/agent-gate'
|
|
71
55
|
|
|
72
56
|
export default defineConfig({
|
|
73
57
|
disabledRules: ['prevent-force-push-main'],
|
|
74
58
|
protectedBranches: ['main', 'release'],
|
|
75
|
-
extraSecretPathPrefixes: ['vault/', 'secrets/'],
|
|
76
59
|
customRules: [
|
|
77
60
|
forbidCommandPattern({
|
|
78
61
|
id: 'no-drop-table',
|
|
79
62
|
match: /drop\s+table/i,
|
|
80
|
-
reason: 'DROP TABLE is forbidden. Use a migration
|
|
81
|
-
}),
|
|
82
|
-
forbidFilePathPattern({
|
|
83
|
-
id: 'no-prod-config',
|
|
84
|
-
match: /production\.ya?ml$/,
|
|
85
|
-
reason: 'Production config edits go through ops review.',
|
|
63
|
+
reason: 'DROP TABLE is forbidden. Use a migration.',
|
|
86
64
|
}),
|
|
87
65
|
],
|
|
88
66
|
})
|
|
89
67
|
```
|
|
90
68
|
|
|
91
|
-
|
|
92
|
-
|---|---|
|
|
93
|
-
| `disabledRules` | List of rule ids that will not run. Merged with `AGENT_GATE_DISABLED_RULES`. |
|
|
94
|
-
| `protectedBranches` | Overrides the default list used by `prevent-force-push-main`. |
|
|
95
|
-
| `extraSecretPathPrefixes` | Additional path substrings treated as secret targets by `prevent-secret-file-write`. |
|
|
96
|
-
| `customRules` | User-defined `DeterministicRule[]` appended after the built-ins. Use the `forbidCommandPattern` / `forbidContentPattern` / `forbidFilePathPattern` factories or hand-write your own. |
|
|
97
|
-
|
|
98
|
-
#### Legacy JSON config
|
|
99
|
-
|
|
100
|
-
Still works for simple cases:
|
|
101
|
-
|
|
102
|
-
```json
|
|
103
|
-
{
|
|
104
|
-
"disabled_rules": ["prevent-force-push-main"],
|
|
105
|
-
"protected_branches": ["main", "release"],
|
|
106
|
-
"extra_secret_paths": ["vault/", "secrets/"]
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
JSON cannot express custom rules. Migrate to the TS/JS form when you need them.
|
|
69
|
+
Full options: see [docs/config.md](docs/config.md) (TODO) or `AgentGatePluginConfig` in the source.
|
|
111
70
|
|
|
112
|
-
##
|
|
71
|
+
## CLI
|
|
113
72
|
|
|
114
|
-
|
|
115
|
-
2. The selected adapter parses that payload into a normalized `Action`.
|
|
116
|
-
3. Deterministic safety rules run first. Catastrophic patterns are blocked here without calling AI.
|
|
117
|
-
4. If the safety baseline passes, agent-gate collects all 8 instruction file formats present in the project tree.
|
|
118
|
-
5. The AI validates the operation against the combined rule set, with each source attributed by kind.
|
|
119
|
-
6. The verdict (block + guidance, or allow) is returned through the adapter's response formatter.
|
|
120
|
-
|
|
121
|
-
## Built-in Safety Rules
|
|
122
|
-
|
|
123
|
-
| Rule | Blocks |
|
|
73
|
+
| Command | What it does |
|
|
124
74
|
|---|---|
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
|
|
131
|
-
Each rule can be disabled individually through `.agent-gate.json` or the env var.
|
|
132
|
-
|
|
133
|
-
## Supported Instruction File Formats
|
|
75
|
+
| `agent-gate` | Run as hook (reads stdin, used internally) |
|
|
76
|
+
| `agent-gate install` / `uninstall` | Register or remove the Claude Code hook |
|
|
77
|
+
| `agent-gate lint` | Audit instruction files for ambiguity, emptiness, missing rules |
|
|
78
|
+
| `agent-gate stats` | Summarize the decision log (after `AGENT_GATE_LOG=1`) |
|
|
79
|
+
| `agent-gate daemon` | Long-lived server on a Unix socket (opt-in speedup, set `AGENT_GATE_DAEMON=1`) |
|
|
134
80
|
|
|
135
|
-
|
|
81
|
+
## Environment
|
|
136
82
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
You do not need to choose. Maintain whichever file your team already uses; agent-gate reads them all.
|
|
147
|
-
|
|
148
|
-
## Supported AI Coding Tools
|
|
149
|
-
|
|
150
|
-
agent-gate enforces in any tool that exposes a pre-tool-use hook. As of v1:
|
|
151
|
-
|
|
152
|
-
- Claude Code (mature). `agent-gate --agent claude-code`, the default.
|
|
153
|
-
- Cursor 1.7 (beta). `agent-gate --agent cursor`. Payload mapping is best-effort against public docs.
|
|
154
|
-
|
|
155
|
-
Tools without a hook surface (Copilot, Cline, Aider, Codex web, Replit, Devin) can still benefit from agent-gate as a rule source linter or via downstream sync (rulesync, symlinks), but cannot be enforced at runtime.
|
|
156
|
-
|
|
157
|
-
## CLAUDE.md Doctor
|
|
158
|
-
|
|
159
|
-
Run `agent-gate lint` from a project root to audit your instruction files for AI-friendliness. The doctor walks the same 8 file formats the runtime reads, then surfaces:
|
|
160
|
-
|
|
161
|
-
- **Empty files** that would make the AI think no rules exist.
|
|
162
|
-
- **Files with no concrete rules** (no imperatives, no bullets, no numbered items).
|
|
163
|
-
- **Ambiguous modifiers** like "where possible", "as needed", "適切に", "なるべく", "可能な限り", "必要に応じて". AI judgment cannot enforce these reliably; the doctor suggests replacing them with a concrete condition or threshold.
|
|
164
|
-
|
|
165
|
-
```text
|
|
166
|
-
$ agent-gate lint
|
|
167
|
-
/p/CLAUDE.md
|
|
168
|
-
[warning] no-concrete-rules: No imperatives ...
|
|
169
|
-
[info] ambiguous-modifier (line 5): Ambiguous modifier "適切に" ...
|
|
170
|
-
> - エラーは適切に扱う
|
|
171
|
-
|
|
172
|
-
1 finding.
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Exit code is 1 if any finding has severity `error`, otherwise 0, so the command can run in CI.
|
|
176
|
-
|
|
177
|
-
## Daemon mode
|
|
178
|
-
|
|
179
|
-
Each hook invocation normally spawns a fresh Node process (cold start ~300ms). For users that fire hooks at high frequency, agent-gate can run as a long-lived daemon on a Unix socket and let hook invocations reuse the warm process.
|
|
180
|
-
|
|
181
|
-
```bash
|
|
182
|
-
# Terminal 1: start the daemon (foreground; manage with systemd / launchctl / tmux in production).
|
|
183
|
-
agent-gate daemon
|
|
184
|
-
|
|
185
|
-
# Terminal 2 (or in your hook config):
|
|
186
|
-
AGENT_GATE_DAEMON=1 agent-gate
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
When `AGENT_GATE_DAEMON=1`, the hook tries the socket first and transparently falls back to direct mode if the daemon is unreachable. Set `AGENT_GATE_SOCKET_PATH` to override the default `$TMPDIR/agent-gate.sock`.
|
|
190
|
-
|
|
191
|
-
The daemon is opt-in. Users not setting `AGENT_GATE_DAEMON=1` keep the existing one-shot behavior.
|
|
192
|
-
|
|
193
|
-
## Observability
|
|
194
|
-
|
|
195
|
-
Set `AGENT_GATE_LOG=1` to append every decision to `~/.agent-gate/log.jsonl`. Each line is a JSON object with timestamp, adapter, tool, decision, reason, source (`deterministic` / `ai`), and `ruleId` when a deterministic rule fired.
|
|
83
|
+
| Var | Effect |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `AGENT_GATE_DISABLED` | Skip all checks |
|
|
86
|
+
| `AGENT_GATE_DISABLED_RULES` | Comma-separated rule ids to skip |
|
|
87
|
+
| `AGENT_GATE_REASON_LANG` | AI reason language: `auto` (default) / `en` / `ja` / etc. |
|
|
88
|
+
| `AGENT_GATE_LOG` | `1` writes decisions to `~/.agent-gate/log.jsonl` |
|
|
89
|
+
| `AGENT_GATE_API_KEY` | Use Anthropic API directly instead of `claude` CLI |
|
|
90
|
+
| `AGENT_GATE_DAEMON` | `1` routes through the daemon if it is running |
|
|
196
91
|
|
|
197
|
-
|
|
92
|
+
## Supported AI tools
|
|
198
93
|
|
|
199
|
-
|
|
94
|
+
- **Claude Code** (mature, default).
|
|
95
|
+
- **Cursor 1.7** (beta, `--agent cursor`; payload mapping is best-effort against public docs).
|
|
200
96
|
|
|
201
|
-
|
|
97
|
+
Other tools (Copilot, Cline, Aider, Codex web, Replit, Devin) lack a hook surface and cannot be enforced at runtime.
|
|
202
98
|
|
|
203
99
|
## License
|
|
204
100
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hiro-c/agent-gate",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Runtime rule enforcer for AI coding agents. Reads CLAUDE.md / AGENTS.md / .cursorrules and enforces them via Claude Code and Cursor hooks, with a deterministic safety baseline.",
|
|
5
5
|
"author": "Hiro-Chiba",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"collectClaudeMd.d.ts","sourceRoot":"","sources":["../../src/collector/collectClaudeMd.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAe9D,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,EAAE,CAY3D"}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.collectClaudeMd = collectClaudeMd;
|
|
4
|
-
const fs_1 = require("fs");
|
|
5
|
-
const path_1 = require("path");
|
|
6
|
-
const EXCLUDED_DIRS = new Set([
|
|
7
|
-
'node_modules',
|
|
8
|
-
'.git',
|
|
9
|
-
'target',
|
|
10
|
-
'.venv',
|
|
11
|
-
'vendor',
|
|
12
|
-
'__pycache__',
|
|
13
|
-
'dist',
|
|
14
|
-
'build',
|
|
15
|
-
]);
|
|
16
|
-
const MAX_DEPTH = 3;
|
|
17
|
-
function collectClaudeMd(cwd) {
|
|
18
|
-
const upward = collectUpward(cwd);
|
|
19
|
-
const downward = collectDownward(cwd);
|
|
20
|
-
// Deduplicate: upward already includes cwd/CLAUDE.md
|
|
21
|
-
const upwardPaths = new Set(upward.map((f) => f.path));
|
|
22
|
-
const combined = [
|
|
23
|
-
...upward,
|
|
24
|
-
...downward.filter((f) => !upwardPaths.has(f.path)),
|
|
25
|
-
];
|
|
26
|
-
return combined;
|
|
27
|
-
}
|
|
28
|
-
function collectUpward(cwd) {
|
|
29
|
-
const files = [];
|
|
30
|
-
let dir = cwd;
|
|
31
|
-
while (true) {
|
|
32
|
-
const claudeMdPath = (0, path_1.join)(dir, 'CLAUDE.md');
|
|
33
|
-
const content = readFileSafe(claudeMdPath);
|
|
34
|
-
if (content !== null) {
|
|
35
|
-
files.push({ path: claudeMdPath, content });
|
|
36
|
-
}
|
|
37
|
-
const parent = (0, path_1.dirname)(dir);
|
|
38
|
-
if (parent === dir)
|
|
39
|
-
break;
|
|
40
|
-
dir = parent;
|
|
41
|
-
}
|
|
42
|
-
return files;
|
|
43
|
-
}
|
|
44
|
-
function collectDownward(cwd) {
|
|
45
|
-
const files = [];
|
|
46
|
-
walkDir(cwd, 0, files);
|
|
47
|
-
return files;
|
|
48
|
-
}
|
|
49
|
-
function walkDir(dir, depth, files) {
|
|
50
|
-
if (depth > MAX_DEPTH)
|
|
51
|
-
return;
|
|
52
|
-
let entries;
|
|
53
|
-
try {
|
|
54
|
-
entries = (0, fs_1.readdirSync)(dir);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
for (const entry of entries) {
|
|
60
|
-
if (EXCLUDED_DIRS.has(entry))
|
|
61
|
-
continue;
|
|
62
|
-
const fullPath = (0, path_1.join)(dir, entry);
|
|
63
|
-
if (entry === 'CLAUDE.md') {
|
|
64
|
-
const content = readFileSafe(fullPath);
|
|
65
|
-
if (content !== null) {
|
|
66
|
-
files.push({ path: fullPath, content });
|
|
67
|
-
}
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
if ((0, fs_1.statSync)(fullPath).isDirectory()) {
|
|
72
|
-
walkDir(fullPath, depth + 1, files);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// Skip inaccessible entries
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function readFileSafe(filePath) {
|
|
81
|
-
try {
|
|
82
|
-
return (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ClaudeMdFile.d.ts","sourceRoot":"","sources":["../../../src/contracts/types/ClaudeMdFile.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA"}
|