@hiro-c/agent-gate 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.
Files changed (154) hide show
  1. package/AGENTS.md +76 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -0
  4. package/dist/adapters/Adapter.d.ts +32 -0
  5. package/dist/adapters/Adapter.d.ts.map +1 -0
  6. package/dist/adapters/Adapter.js +2 -0
  7. package/dist/adapters/claude-code/adapter.d.ts +3 -0
  8. package/dist/adapters/claude-code/adapter.d.ts.map +1 -0
  9. package/dist/adapters/claude-code/adapter.js +45 -0
  10. package/dist/adapters/claude-code/transcript.d.ts +16 -0
  11. package/dist/adapters/claude-code/transcript.d.ts.map +1 -0
  12. package/dist/adapters/claude-code/transcript.js +104 -0
  13. package/dist/adapters/cursor/adapter.d.ts +3 -0
  14. package/dist/adapters/cursor/adapter.d.ts.map +1 -0
  15. package/dist/adapters/cursor/adapter.js +89 -0
  16. package/dist/adapters/index.d.ts +8 -0
  17. package/dist/adapters/index.d.ts.map +1 -0
  18. package/dist/adapters/index.js +20 -0
  19. package/dist/cli/agent-gate.d.ts +13 -0
  20. package/dist/cli/agent-gate.d.ts.map +1 -0
  21. package/dist/cli/agent-gate.js +226 -0
  22. package/dist/cli/installer.d.ts +21 -0
  23. package/dist/cli/installer.d.ts.map +1 -0
  24. package/dist/cli/installer.js +71 -0
  25. package/dist/collector/collectClaudeMd.d.ts +3 -0
  26. package/dist/collector/collectClaudeMd.d.ts.map +1 -0
  27. package/dist/collector/collectClaudeMd.js +87 -0
  28. package/dist/collector/collectRuleSources.d.ts +3 -0
  29. package/dist/collector/collectRuleSources.d.ts.map +1 -0
  30. package/dist/collector/collectRuleSources.js +151 -0
  31. package/dist/config/AgentGateConfig.d.ts +18 -0
  32. package/dist/config/AgentGateConfig.d.ts.map +1 -0
  33. package/dist/config/AgentGateConfig.js +34 -0
  34. package/dist/config/Config.d.ts +26 -0
  35. package/dist/config/Config.d.ts.map +1 -0
  36. package/dist/config/Config.js +25 -0
  37. package/dist/config/PluginConfigLoader.d.ts +3 -0
  38. package/dist/config/PluginConfigLoader.d.ts.map +1 -0
  39. package/dist/config/PluginConfigLoader.js +85 -0
  40. package/dist/config/defineConfig.d.ts +23 -0
  41. package/dist/config/defineConfig.d.ts.map +1 -0
  42. package/dist/config/defineConfig.js +10 -0
  43. package/dist/contracts/schemas/hookDataSchema.d.ts +7 -0
  44. package/dist/contracts/schemas/hookDataSchema.d.ts.map +1 -0
  45. package/dist/contracts/schemas/hookDataSchema.js +9 -0
  46. package/dist/contracts/types/Action.d.ts +23 -0
  47. package/dist/contracts/types/Action.d.ts.map +1 -0
  48. package/dist/contracts/types/Action.js +2 -0
  49. package/dist/contracts/types/ClaudeMdFile.d.ts +5 -0
  50. package/dist/contracts/types/ClaudeMdFile.d.ts.map +1 -0
  51. package/dist/contracts/types/ClaudeMdFile.js +2 -0
  52. package/dist/contracts/types/HookData.d.ts +6 -0
  53. package/dist/contracts/types/HookData.d.ts.map +1 -0
  54. package/dist/contracts/types/HookData.js +2 -0
  55. package/dist/contracts/types/ModelClient.d.ts +4 -0
  56. package/dist/contracts/types/ModelClient.d.ts.map +1 -0
  57. package/dist/contracts/types/ModelClient.js +2 -0
  58. package/dist/contracts/types/RuleSource.d.ts +7 -0
  59. package/dist/contracts/types/RuleSource.d.ts.map +1 -0
  60. package/dist/contracts/types/RuleSource.js +2 -0
  61. package/dist/contracts/types/SessionContext.d.ts +25 -0
  62. package/dist/contracts/types/SessionContext.d.ts.map +1 -0
  63. package/dist/contracts/types/SessionContext.js +2 -0
  64. package/dist/contracts/types/ValidationResult.d.ts +5 -0
  65. package/dist/contracts/types/ValidationResult.d.ts.map +1 -0
  66. package/dist/contracts/types/ValidationResult.js +2 -0
  67. package/dist/daemon/client.d.ts +17 -0
  68. package/dist/daemon/client.d.ts.map +1 -0
  69. package/dist/daemon/client.js +59 -0
  70. package/dist/daemon/protocol.d.ts +17 -0
  71. package/dist/daemon/protocol.d.ts.map +1 -0
  72. package/dist/daemon/protocol.js +8 -0
  73. package/dist/daemon/server.d.ts +27 -0
  74. package/dist/daemon/server.d.ts.map +1 -0
  75. package/dist/daemon/server.js +100 -0
  76. package/dist/deterministic/defaultRules.d.ts +11 -0
  77. package/dist/deterministic/defaultRules.d.ts.map +1 -0
  78. package/dist/deterministic/defaultRules.js +33 -0
  79. package/dist/deterministic/engine.d.ts +11 -0
  80. package/dist/deterministic/engine.d.ts.map +1 -0
  81. package/dist/deterministic/engine.js +12 -0
  82. package/dist/deterministic/factories.d.ts +20 -0
  83. package/dist/deterministic/factories.d.ts.map +1 -0
  84. package/dist/deterministic/factories.js +56 -0
  85. package/dist/deterministic/rules/preventBashSecretWrite.d.ts +3 -0
  86. package/dist/deterministic/rules/preventBashSecretWrite.d.ts.map +1 -0
  87. package/dist/deterministic/rules/preventBashSecretWrite.js +75 -0
  88. package/dist/deterministic/rules/preventForcePushMain.d.ts +7 -0
  89. package/dist/deterministic/rules/preventForcePushMain.d.ts.map +1 -0
  90. package/dist/deterministic/rules/preventForcePushMain.js +85 -0
  91. package/dist/deterministic/rules/preventRmRfRoot.d.ts +3 -0
  92. package/dist/deterministic/rules/preventRmRfRoot.d.ts.map +1 -0
  93. package/dist/deterministic/rules/preventRmRfRoot.js +68 -0
  94. package/dist/deterministic/rules/preventSecretFileWrite.d.ts +7 -0
  95. package/dist/deterministic/rules/preventSecretFileWrite.d.ts.map +1 -0
  96. package/dist/deterministic/rules/preventSecretFileWrite.js +55 -0
  97. package/dist/deterministic/rules/preventSystemPathWrite.d.ts +3 -0
  98. package/dist/deterministic/rules/preventSystemPathWrite.d.ts.map +1 -0
  99. package/dist/deterministic/rules/preventSystemPathWrite.js +38 -0
  100. package/dist/deterministic/types.d.ts +20 -0
  101. package/dist/deterministic/types.d.ts.map +1 -0
  102. package/dist/deterministic/types.js +2 -0
  103. package/dist/doctor/findings.d.ts +15 -0
  104. package/dist/doctor/findings.d.ts.map +1 -0
  105. package/dist/doctor/findings.js +2 -0
  106. package/dist/doctor/formatFindings.d.ts +3 -0
  107. package/dist/doctor/formatFindings.d.ts.map +1 -0
  108. package/dist/doctor/formatFindings.js +37 -0
  109. package/dist/doctor/lintRuleSources.d.ts +4 -0
  110. package/dist/doctor/lintRuleSources.d.ts.map +1 -0
  111. package/dist/doctor/lintRuleSources.js +87 -0
  112. package/dist/hooks/processHookData.d.ts +37 -0
  113. package/dist/hooks/processHookData.d.ts.map +1 -0
  114. package/dist/hooks/processHookData.js +181 -0
  115. package/dist/index.d.ts +38 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +54 -0
  118. package/dist/observability/decisionLogger.d.ts +15 -0
  119. package/dist/observability/decisionLogger.d.ts.map +1 -0
  120. package/dist/observability/decisionLogger.js +20 -0
  121. package/dist/observability/eventBus.d.ts +15 -0
  122. package/dist/observability/eventBus.d.ts.map +1 -0
  123. package/dist/observability/eventBus.js +33 -0
  124. package/dist/observability/sinks/JsonlFileSink.d.ts +13 -0
  125. package/dist/observability/sinks/JsonlFileSink.d.ts.map +1 -0
  126. package/dist/observability/sinks/JsonlFileSink.js +36 -0
  127. package/dist/observability/sinks/Sink.d.ts +46 -0
  128. package/dist/observability/sinks/Sink.d.ts.map +1 -0
  129. package/dist/observability/sinks/Sink.js +2 -0
  130. package/dist/observability/stats.d.ts +14 -0
  131. package/dist/observability/stats.d.ts.map +1 -0
  132. package/dist/observability/stats.js +78 -0
  133. package/dist/validation/models/AnthropicApi.d.ts +9 -0
  134. package/dist/validation/models/AnthropicApi.d.ts.map +1 -0
  135. package/dist/validation/models/AnthropicApi.js +44 -0
  136. package/dist/validation/models/ClaudeCli.d.ts +10 -0
  137. package/dist/validation/models/ClaudeCli.d.ts.map +1 -0
  138. package/dist/validation/models/ClaudeCli.js +64 -0
  139. package/dist/validation/models/CompositeModelClient.d.ts +20 -0
  140. package/dist/validation/models/CompositeModelClient.d.ts.map +1 -0
  141. package/dist/validation/models/CompositeModelClient.js +53 -0
  142. package/dist/validation/prompts/context.d.ts +3 -0
  143. package/dist/validation/prompts/context.d.ts.map +1 -0
  144. package/dist/validation/prompts/context.js +29 -0
  145. package/dist/validation/prompts/response.d.ts +2 -0
  146. package/dist/validation/prompts/response.d.ts.map +1 -0
  147. package/dist/validation/prompts/response.js +20 -0
  148. package/dist/validation/prompts/system-prompt.d.ts +7 -0
  149. package/dist/validation/prompts/system-prompt.d.ts.map +1 -0
  150. package/dist/validation/prompts/system-prompt.js +69 -0
  151. package/dist/validation/validator.d.ts +5 -0
  152. package/dist/validation/validator.d.ts.map +1 -0
  153. package/dist/validation/validator.js +98 -0
  154. package/package.json +67 -0
package/AGENTS.md ADDED
@@ -0,0 +1,76 @@
1
+ # AGENTS.md for agent-gate
2
+
3
+ This file is read by any AI coding agent working on this repo, including
4
+ Claude Code, Cursor, Cline, and Aider. It is the project's vendor-neutral
5
+ instruction file, complementing the existing `CLAUDE.md`.
6
+
7
+ ## What this project is
8
+
9
+ agent-gate is a runtime enforcer for AI coding agent rules. It reads
10
+ natural-language instruction files (CLAUDE.md, AGENTS.md, .cursorrules,
11
+ .cursor/rules/*.mdc, .clinerules, .windsurf/rules, .github/copilot-instructions.md,
12
+ CONVENTIONS.md) as one combined rule set and enforces them at hook time
13
+ in Claude Code and Cursor. A deterministic safety baseline catches
14
+ catastrophic operations before any AI call.
15
+
16
+ ## Hard rules
17
+
18
+ These are non-negotiable while modifying this codebase.
19
+
20
+ - Never weaken the deterministic safety rules to make a test pass. If a
21
+ rule blocks something it should not, narrow the rule by adding an
22
+ explicit allow-case, never by deleting the protection.
23
+ - Never commit any file matching `.env`, `.env.*` (other than
24
+ `.env.example`), `*.pem`, `*.key`, or files inside `.ssh/`.
25
+ - Never run `npm publish` from a local checkout. Publishing is performed
26
+ exclusively by the GitHub Actions release workflow on a signed `v*`
27
+ tag push (using OIDC + npm provenance). This keeps releases
28
+ reproducible and supply-chain auditable.
29
+
30
+ ## Soft rules (style and approach)
31
+
32
+ - Strict TDD for every new rule: write a failing test, make it pass with
33
+ the smallest possible change, refactor, commit.
34
+ - One logical change per commit. Use Conventional Commits prefixes
35
+ (`feat`, `fix`, `refactor`, `chore`, `docs`, `test`).
36
+ - Prefer adding a new deterministic rule over expanding an existing one
37
+ when the concept is distinct; small, single-purpose rules are easier
38
+ to disable per project.
39
+ - Keep block reasons guidance-shaped: name the violated rule and the
40
+ next correct step the agent should take. No bare denials.
41
+ - Tests live alongside the file they cover: `test/<mirror of src path>`.
42
+
43
+ ## Development workflow
44
+
45
+ ```bash
46
+ npm install
47
+ npm run checks # typecheck + vitest
48
+ npm run build # tsc to dist/
49
+ npm test # vitest run
50
+ npm run typecheck # tsc --noEmit
51
+ ```
52
+
53
+ Every change should keep `npm run checks` green before being committed.
54
+
55
+ ## Project structure
56
+
57
+ - `src/adapters/` — vendor-specific hook payload parsing and response
58
+ formatting (Claude Code, Cursor).
59
+ - `src/collector/` — multi-source instruction-file aggregator.
60
+ - `src/config/` — `Config` (env vars) and `AgentGateConfig`
61
+ (`.agent-gate.json`).
62
+ - `src/contracts/` — shared types (`Action`, `RuleSource`,
63
+ `ValidationResult`) and Zod schemas.
64
+ - `src/deterministic/` — the safety baseline. `engine.ts` runs rules
65
+ in order, `rules/*` are individual rules, `defaultRules.ts` exports
66
+ the curated default list.
67
+ - `src/hooks/processHookData.ts` — pipeline orchestrator.
68
+ - `src/observability/` — decision logger and stats aggregator.
69
+ - `src/validation/` — AI client interface, model implementations
70
+ (`ClaudeCli`, `AnthropicApi`), prompt templates.
71
+
72
+ ## When in doubt
73
+
74
+ Read `docs/architecture.md` (kept locally, gitignored) for the v1
75
+ roadmap and design rationale. Otherwise, prefer the smallest change
76
+ that keeps `npm run checks` green and ship it.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hiro-Chiba
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # agent-gate
2
+
3
+ [![CI](https://github.com/Hiro-Chiba/agent-gate/actions/workflows/ci.yml/badge.svg)](https://github.com/Hiro-Chiba/agent-gate/actions/workflows/ci.yml)
4
+
5
+ One natural-language rule source, enforced at runtime across multiple AI coding tools.
6
+
7
+ agent-gate reads the instruction files you already have (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.cursor/rules/*.mdc`, `.clinerules`, `.windsurf/rules`, `.github/copilot-instructions.md`, `CONVENTIONS.md`) as a single combined rule set, then enforces them at hook time in Claude Code and Cursor. A deterministic safety baseline catches catastrophic operations before any AI call.
8
+
9
+ ## Why
10
+
11
+ The AI coding tool landscape in 2026 has fragmented into many tools, each with its own instruction file format. Existing tooling either syncs rule files across tools (rulesync, symlinks) or enforces a single rule format at runtime (probity, tdd-guard), but not both. agent-gate sits in the gap: it accepts whatever natural-language instruction files you already maintain and enforces them across multiple AI tools through a single hook.
12
+
13
+ Two pain points the project is built to address:
14
+
15
+ - **Rule forgetting** in long agent sessions, where context compression drops the rules from the prompt and the agent quietly drifts off-spec.
16
+ - **Destructive operations** like `rm -rf $HOME` or force-pushing main, which AI judgment is too unreliable to catch consistently.
17
+
18
+ ## Features
19
+
20
+ - Multi-source rule collection across 8 instruction file formats, surfaced to AI with per-source attribution.
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.
27
+
28
+ ## Requirements
29
+
30
+ - Node.js >= 22.0.0
31
+ - Claude Code or Cursor 1.7
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ git clone https://github.com/Hiro-Chiba/agent-gate.git
37
+ cd agent-gate
38
+ ./install.sh
39
+ ```
40
+
41
+ The install script handles dependency install, build, and Claude Code hook registration. Restart Claude Code to activate.
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
48
+
49
+ | Variable | Default | Description |
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) |
59
+
60
+ ### Project config file
61
+
62
+ agent-gate looks for either a TypeScript / JavaScript config or a legacy JSON config, walking upward from the cwd until it finds one. Precedence (highest wins): `.agent-gate.config.ts` > `.mts` > `.mjs` > `.cjs` > `.js` > `.agent-gate.json`.
63
+
64
+ #### TypeScript / JavaScript config
65
+
66
+ The full API including custom rules:
67
+
68
+ ```ts
69
+ // .agent-gate.config.ts
70
+ import { defineConfig, forbidCommandPattern, forbidFilePathPattern } from 'agent-gate'
71
+
72
+ export default defineConfig({
73
+ disabledRules: ['prevent-force-push-main'],
74
+ protectedBranches: ['main', 'release'],
75
+ extraSecretPathPrefixes: ['vault/', 'secrets/'],
76
+ customRules: [
77
+ forbidCommandPattern({
78
+ id: 'no-drop-table',
79
+ match: /drop\s+table/i,
80
+ reason: 'DROP TABLE is forbidden. Use a migration instead.',
81
+ }),
82
+ forbidFilePathPattern({
83
+ id: 'no-prod-config',
84
+ match: /production\.ya?ml$/,
85
+ reason: 'Production config edits go through ops review.',
86
+ }),
87
+ ],
88
+ })
89
+ ```
90
+
91
+ | Field | Effect |
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.
111
+
112
+ ## How It Works
113
+
114
+ 1. The AI coding tool fires a pre-tool-use hook with its vendor-specific JSON payload.
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 |
124
+ |---|---|
125
+ | `prevent-rm-rf-root` | Recursive `rm` on `/`, `$HOME`, `~`, `/etc`, `/usr`, `/var`, and other catastrophic paths. Handles `sudo` prefix and flag variants (`-rf`, `-fr`, `-Rf`). |
126
+ | `prevent-secret-file-write` | `Edit`/`Write` on `.env*` (non-template), `.ssh/*`, `.aws/credentials`, `*.pem`, `*.key`, `id_rsa`, `.netrc`. |
127
+ | `prevent-bash-secret-write` | Shell redirects to the same secret paths via `>`, `>>`, or `tee`. |
128
+ | `prevent-force-push-main` | `git push --force` or `-f` to `main`, `master`, `develop`, `production`, `release`, `stable`. Allows `--force-with-lease`. |
129
+ | `prevent-system-path-write` | `Edit`/`Write` to `/etc`, `/usr`, `/var`, `/System`, `/Library`, `/opt`, and other system-owned paths. |
130
+
131
+ Each rule can be disabled individually through `.agent-gate.json` or the env var.
132
+
133
+ ## Supported Instruction File Formats
134
+
135
+ agent-gate aggregates rules from any combination of:
136
+
137
+ - `CLAUDE.md` (Claude Code)
138
+ - `AGENTS.md` (cross-tool spec backed by the Linux Foundation Agentic AI Foundation)
139
+ - `.cursorrules` (Cursor legacy)
140
+ - `.cursor/rules/*.mdc` (Cursor current)
141
+ - `.clinerules/*.md` (Cline)
142
+ - `.windsurf/rules/*.md` (Windsurf)
143
+ - `.github/copilot-instructions.md` (GitHub Copilot)
144
+ - `CONVENTIONS.md` (Aider)
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.
196
+
197
+ Run `agent-gate stats` for a summary: total decisions, block percentage, breakdown by source, adapter, tool, and rule id.
198
+
199
+ ## Network Access
200
+
201
+ agent-gate only communicates with Anthropic endpoints, either directly via the Anthropic API (when `AGENT_GATE_API_KEY` is set) or indirectly through the Claude CLI subprocess. It does not contact any other external services and does not send telemetry. Deterministic rules run entirely locally.
202
+
203
+ ## License
204
+
205
+ [MIT](LICENSE)
@@ -0,0 +1,32 @@
1
+ import { ParsedHook } from '../contracts/types/Action';
2
+ import { ValidationResult } from '../contracts/types/ValidationResult';
3
+ import { SessionEvent } from '../contracts/types/SessionContext';
4
+ export interface ReadHistoryOptions {
5
+ cwd: string;
6
+ /** Max events to return. Adapters should honor this; default is impl-defined. */
7
+ limit?: number;
8
+ }
9
+ /**
10
+ * An Adapter bridges a specific AI coding tool's hook API to agent-gate's
11
+ * normalized pipeline. Each adapter:
12
+ * 1. parses the vendor's stdin JSON into a ParsedHook,
13
+ * 2. formats agent-gate's ValidationResult into the vendor's expected
14
+ * stdout JSON,
15
+ * 3. optionally reads the vendor's transcript file to provide
16
+ * SessionContext.history to rules and the AI prompt.
17
+ */
18
+ export interface Adapter {
19
+ /** Identifier used for CLI dispatch and logging. */
20
+ readonly id: string;
21
+ /** Parse stdin JSON into a normalized ParsedHook. */
22
+ parseHook(stdinJson: string): ParsedHook;
23
+ /** Format a ValidationResult into the vendor-specific stdout JSON. */
24
+ formatResponse(result: ValidationResult): string;
25
+ /**
26
+ * Read recent session events from the vendor's transcript. Optional;
27
+ * an adapter that cannot read history should omit this method or
28
+ * return an empty array.
29
+ */
30
+ readHistory?(opts: ReadHistoryOptions): Promise<SessionEvent[]>;
31
+ }
32
+ //# sourceMappingURL=Adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/Adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAA;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAA;AAEhE,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,OAAO;IACtB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IAEnB,qDAAqD;IACrD,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAAA;IAExC,sEAAsE;IACtE,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAAA;IAEhD;;;;OAIG;IACH,WAAW,CAAC,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAA;CAChE"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ import { Adapter } from '../Adapter';
2
+ export declare const claudeCodeAdapter: Adapter;
3
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/claude-code/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAsB,MAAM,YAAY,CAAA;AASxD,eAAO,MAAM,iBAAiB,EAAE,OA4C/B,CAAA"}
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.claudeCodeAdapter = void 0;
4
+ const hookDataSchema_1 = require("../../contracts/schemas/hookDataSchema");
5
+ const transcript_1 = require("./transcript");
6
+ const HOOK_EVENT_PRE_TOOL_USE = 'PreToolUse';
7
+ exports.claudeCodeAdapter = {
8
+ id: 'claude-code',
9
+ parseHook(stdinJson) {
10
+ let raw;
11
+ try {
12
+ raw = JSON.parse(stdinJson);
13
+ }
14
+ catch {
15
+ return { kind: 'skip', reason: 'invalid JSON' };
16
+ }
17
+ const parsed = hookDataSchema_1.HookDataSchema.safeParse(raw);
18
+ if (!parsed.success) {
19
+ return { kind: 'skip', reason: 'unrecognized hook payload' };
20
+ }
21
+ const data = parsed.data;
22
+ if (data.hook_event_name !== HOOK_EVENT_PRE_TOOL_USE) {
23
+ return { kind: 'skip', reason: `not a PreToolUse event: ${data.hook_event_name}` };
24
+ }
25
+ const toolName = data.tool_name;
26
+ const toolInput = data.tool_input;
27
+ if (!toolName || !toolInput) {
28
+ return { kind: 'skip', reason: 'missing tool_name or tool_input' };
29
+ }
30
+ return {
31
+ kind: 'action',
32
+ action: { toolName, toolInput },
33
+ };
34
+ },
35
+ formatResponse(result) {
36
+ if (result.decision === 'block') {
37
+ return JSON.stringify({ decision: 'block', reason: result.reason });
38
+ }
39
+ // Allow case: Claude Code expects no `decision` key (or null) for allow.
40
+ return JSON.stringify({ reason: result.reason });
41
+ },
42
+ async readHistory(opts) {
43
+ return (0, transcript_1.readClaudeCodeTranscript)({ cwd: opts.cwd, limit: opts.limit });
44
+ },
45
+ };
@@ -0,0 +1,16 @@
1
+ import { SessionEvent } from '../../contracts/types/SessionContext';
2
+ /**
3
+ * Encode a project path into the directory name Claude Code uses under
4
+ * `~/.claude/projects/`. The convention: replace `/` with `-`, drop
5
+ * trailing slash. e.g. `/Users/me/proj` -> `-Users-me-proj`.
6
+ */
7
+ export declare function encodeProjectsPath(cwd: string): string;
8
+ export interface ReadClaudeCodeTranscriptOptions {
9
+ cwd: string;
10
+ /** Override $HOME for testing. Defaults to os.homedir(). */
11
+ home?: string;
12
+ /** Max events to return. Returns the last `limit` events. */
13
+ limit?: number;
14
+ }
15
+ export declare function readClaudeCodeTranscript(opts: ReadClaudeCodeTranscriptOptions): Promise<SessionEvent[]>;
16
+ //# sourceMappingURL=transcript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../../src/adapters/claude-code/transcript.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAA;AAEnE;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMtD;AAED,MAAM,WAAW,+BAA+B;IAC9C,GAAG,EAAE,MAAM,CAAA;IACX,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AA6DD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,+BAA+B,GACpC,OAAO,CAAC,YAAY,EAAE,CAAC,CA+BzB"}
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encodeProjectsPath = encodeProjectsPath;
4
+ exports.readClaudeCodeTranscript = readClaudeCodeTranscript;
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ const os_1 = require("os");
8
+ /**
9
+ * Encode a project path into the directory name Claude Code uses under
10
+ * `~/.claude/projects/`. The convention: replace `/` with `-`, drop
11
+ * trailing slash. e.g. `/Users/me/proj` -> `-Users-me-proj`.
12
+ */
13
+ function encodeProjectsPath(cwd) {
14
+ let trimmed = cwd;
15
+ if (trimmed.length > 1 && trimmed.endsWith('/')) {
16
+ trimmed = trimmed.slice(0, -1);
17
+ }
18
+ return trimmed.replace(/\//g, '-');
19
+ }
20
+ function classify(raw) {
21
+ const t = raw.type;
22
+ if (t === 'tool_use') {
23
+ return {
24
+ kind: 'tool-call',
25
+ toolName: typeof raw.name === 'string' ? raw.name : undefined,
26
+ toolInput: raw.input && typeof raw.input === 'object'
27
+ ? raw.input
28
+ : undefined,
29
+ timestamp: raw.timestamp,
30
+ raw,
31
+ };
32
+ }
33
+ if (t === 'tool_result') {
34
+ return {
35
+ kind: 'tool-result',
36
+ timestamp: raw.timestamp,
37
+ raw,
38
+ };
39
+ }
40
+ if (t === 'user') {
41
+ return { kind: 'user-message', timestamp: raw.timestamp, raw };
42
+ }
43
+ if (t === 'assistant') {
44
+ return { kind: 'assistant-message', timestamp: raw.timestamp, raw };
45
+ }
46
+ return null;
47
+ }
48
+ function pickMostRecentJsonl(dir) {
49
+ let entries;
50
+ try {
51
+ entries = (0, fs_1.readdirSync)(dir).filter((name) => name.endsWith('.jsonl'));
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ if (entries.length === 0)
57
+ return null;
58
+ let best = null;
59
+ for (const name of entries) {
60
+ try {
61
+ const m = (0, fs_1.statSync)((0, path_1.join)(dir, name)).mtimeMs;
62
+ if (!best || m > best.mtime)
63
+ best = { name, mtime: m };
64
+ }
65
+ catch {
66
+ // skip
67
+ }
68
+ }
69
+ return best ? (0, path_1.join)(dir, best.name) : null;
70
+ }
71
+ async function readClaudeCodeTranscript(opts) {
72
+ const home = opts.home ?? (0, os_1.homedir)();
73
+ const dir = (0, path_1.join)(home, '.claude', 'projects', encodeProjectsPath(opts.cwd));
74
+ if (!(0, fs_1.existsSync)(dir))
75
+ return [];
76
+ const file = pickMostRecentJsonl(dir);
77
+ if (!file)
78
+ return [];
79
+ let content;
80
+ try {
81
+ content = (0, fs_1.readFileSync)(file, 'utf-8');
82
+ }
83
+ catch {
84
+ return [];
85
+ }
86
+ const events = [];
87
+ for (const line of content.split('\n')) {
88
+ const trimmed = line.trim();
89
+ if (!trimmed)
90
+ continue;
91
+ let raw;
92
+ try {
93
+ raw = JSON.parse(trimmed);
94
+ }
95
+ catch {
96
+ continue;
97
+ }
98
+ const evt = classify(raw);
99
+ if (evt)
100
+ events.push(evt);
101
+ }
102
+ const limit = opts.limit ?? events.length;
103
+ return events.slice(-limit);
104
+ }
@@ -0,0 +1,3 @@
1
+ import { Adapter } from '../Adapter';
2
+ export declare const cursorAdapter: Adapter;
3
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/cursor/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AA2BpC,eAAO,MAAM,aAAa,EAAE,OAoE3B,CAAA"}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cursorAdapter = void 0;
4
+ /**
5
+ * Cursor 1.7 hook adapter.
6
+ *
7
+ * Cursor exposes pre-execution hooks (beforeShellExecution, beforeReadFile,
8
+ * beforeFileEdit) and post-event hooks (afterFileEdit). agent-gate is a
9
+ * prevention tool, so it only handles the "before" hooks; "after" payloads
10
+ * are skipped because the operation already happened.
11
+ *
12
+ * The payload schema below is the best-effort shape based on Cursor's
13
+ * public docs as of early 2026. Field names may need adjustment as
14
+ * Cursor's hook API matures.
15
+ */
16
+ const PRE_HOOK_EVENTS = new Set([
17
+ 'beforeShellExecution',
18
+ 'beforeReadFile',
19
+ 'beforeFileEdit',
20
+ ]);
21
+ function isObject(v) {
22
+ return typeof v === 'object' && v !== null;
23
+ }
24
+ exports.cursorAdapter = {
25
+ id: 'cursor',
26
+ parseHook(stdinJson) {
27
+ let raw;
28
+ try {
29
+ raw = JSON.parse(stdinJson);
30
+ }
31
+ catch {
32
+ return { kind: 'skip', reason: 'invalid JSON' };
33
+ }
34
+ if (!isObject(raw)) {
35
+ return { kind: 'skip', reason: 'payload is not an object' };
36
+ }
37
+ const event = raw['hook_event_name'];
38
+ if (typeof event !== 'string' || !PRE_HOOK_EVENTS.has(event)) {
39
+ return { kind: 'skip', reason: `not a pre-hook event: ${String(event)}` };
40
+ }
41
+ switch (event) {
42
+ case 'beforeShellExecution': {
43
+ const command = raw['command'];
44
+ if (typeof command !== 'string') {
45
+ return { kind: 'skip', reason: 'missing command' };
46
+ }
47
+ return {
48
+ kind: 'action',
49
+ action: { toolName: 'Bash', toolInput: { command } },
50
+ };
51
+ }
52
+ case 'beforeReadFile': {
53
+ const filePath = raw['file_path'];
54
+ if (typeof filePath !== 'string') {
55
+ return { kind: 'skip', reason: 'missing file_path' };
56
+ }
57
+ return {
58
+ kind: 'action',
59
+ action: { toolName: 'Read', toolInput: { file_path: filePath } },
60
+ };
61
+ }
62
+ case 'beforeFileEdit': {
63
+ const filePath = raw['file_path'];
64
+ if (typeof filePath !== 'string') {
65
+ return { kind: 'skip', reason: 'missing file_path' };
66
+ }
67
+ const toolInput = { file_path: filePath };
68
+ if (typeof raw['new_content'] === 'string') {
69
+ toolInput.new_content = raw['new_content'];
70
+ }
71
+ return {
72
+ kind: 'action',
73
+ action: { toolName: 'Edit', toolInput },
74
+ };
75
+ }
76
+ default:
77
+ return { kind: 'skip', reason: 'unrecognized event' };
78
+ }
79
+ },
80
+ formatResponse(result) {
81
+ if (result.decision === 'block') {
82
+ return JSON.stringify({
83
+ permission: 'deny',
84
+ userMessage: result.reason,
85
+ });
86
+ }
87
+ return JSON.stringify({ permission: 'allow' });
88
+ },
89
+ };
@@ -0,0 +1,8 @@
1
+ import { Adapter } from './Adapter';
2
+ import { claudeCodeAdapter } from './claude-code/adapter';
3
+ import { cursorAdapter } from './cursor/adapter';
4
+ export declare const DEFAULT_ADAPTER_ID = "claude-code";
5
+ export declare function getAdapter(id: string): Adapter | undefined;
6
+ export declare function availableAdapterIds(): string[];
7
+ export { Adapter, claudeCodeAdapter, cursorAdapter };
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAEhD,eAAO,MAAM,kBAAkB,gBAAgB,CAAA;AAO/C,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAE1D;AAED,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAE9C;AAED,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,CAAA"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cursorAdapter = exports.claudeCodeAdapter = exports.DEFAULT_ADAPTER_ID = void 0;
4
+ exports.getAdapter = getAdapter;
5
+ exports.availableAdapterIds = availableAdapterIds;
6
+ const adapter_1 = require("./claude-code/adapter");
7
+ Object.defineProperty(exports, "claudeCodeAdapter", { enumerable: true, get: function () { return adapter_1.claudeCodeAdapter; } });
8
+ const adapter_2 = require("./cursor/adapter");
9
+ Object.defineProperty(exports, "cursorAdapter", { enumerable: true, get: function () { return adapter_2.cursorAdapter; } });
10
+ exports.DEFAULT_ADAPTER_ID = 'claude-code';
11
+ const ADAPTER_REGISTRY = {
12
+ 'claude-code': adapter_1.claudeCodeAdapter,
13
+ cursor: adapter_2.cursorAdapter,
14
+ };
15
+ function getAdapter(id) {
16
+ return ADAPTER_REGISTRY[id];
17
+ }
18
+ function availableAdapterIds() {
19
+ return Object.keys(ADAPTER_REGISTRY).sort();
20
+ }