Skip to content

tak848/ccgate

Repository files navigation

ccgate

CI release

A PermissionRequest hook for AI coding tools. It delegates each tool-execution permission decision to an LLM (Claude Haiku) using rules written in a jsonnet configuration file.

ccgate ships with built-in default rules, so it works out of the box without any configuration.

ccgate in action: a safe echo is allowed while curl ... | bash is denied with a deny_message

Supported targets:

日本語ドキュメント

Install

mise (recommended)

Requires mise 2026.4.20 or later.

mise use -g aqua:tak848/ccgate

To try ccgate without installing it globally (similar to npx / uvx):

mise exec aqua:tak848/ccgate -- ccgate --version

aqua

Via the aqua standard registry (requires registry v4.498.0 or later). In an aqua-managed project (run aqua init first if you don't have an aqua.yaml yet):

aqua g -i tak848/ccgate
aqua i

For a global aqua config, follow aqua's own tutorial.

go install

go install github.com/tak848/ccgate@latest

GitHub Releases

Download a binary from Releases and place it on your PATH.

Homebrew

brew install tak848/tap/ccgate

Quick start — Claude Code

1. Register as a Claude Code hook

~/.claude/settings.json:

{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "ccgate claude"
          }
        ]
      }
    ]
  }
}

"command": "ccgate" (no subcommand) is the canonical Claude Code hook invocation; ccgate claude is the explicit form.

If ccgate is not on your PATH (e.g. when relying on mise exec instead of a global install), set the hook command to the equivalent invocation, or use an absolute path to the binary.

2. API key

ccgate calls Anthropic's Claude Haiku by default. Export CCGATE_ANTHROPIC_API_KEY (or ANTHROPIC_API_KEY as fallback). For OpenAI / Gemini and the resolution order, see docs/providers.md#api-keys.

That's it — ccgate is now running with its embedded defaults. To customize what is allowed or denied, see docs/rule-tuning.md; for background on how rules work, see Concepts.

Quick start — Codex CLI

Note

Codex hooks require [features] codex_hooks = true in ~/.codex/config.toml. See docs/codex-cli.md for details.

1. Register as a Codex hook

Codex reads hooks from ~/.codex/hooks.json and ~/.codex/config.toml (with <repo>/.codex/{hooks.json,config.toml} overlays once the project is trusted). Pick whichever fits your setup.

~/.codex/hooks.json:

{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "ccgate codex",
            "statusMessage": "ccgate evaluating request"
          }
        ]
      }
    ]
  }
}

~/.codex/config.toml:

[features]
codex_hooks = true   # Codex hooks live behind this feature flag; keep it set for compatibility.

[[hooks.PermissionRequest]]
matcher = ""

[[hooks.PermissionRequest.hooks]]
type    = "command"
command = "ccgate codex"
statusMessage = "ccgate evaluating request"

See docs/codex-cli.md for the full lookup order, project-local overlays, and a go run recipe for in-tree dev builds.

2. API key

Export the provider API key — see docs/providers.md#api-keys.

That's it — ccgate is now running with its embedded defaults. To customize what is allowed or denied, see docs/rule-tuning.md; for background on how rules work, see Concepts.

Concepts

ccgate's allow / deny / environment lists are strings of natural-language guidance that get embedded into a system prompt and sent to the LLM. They are not patterns matched by a deterministic engine — every PermissionRequest goes through the LLM, and the LLM classifies it as allow, deny, or fallthrough based on the rules plus the request context.

Evaluation flow:

flowchart TD
  A["Claude Code / Codex CLI"] --> B{"Resolved by the upstream tool's static rules?"}
  B -->|Yes| C["Run / refuse upstream"]
  B -->|No| D["PermissionRequest hook<br/>(stdin: HookInput JSON)"]
  D --> E["ccgate"]
  E --> F1["Load jsonnet config<br/>embedded defaults + global + project-local"]
  E --> F2["Build context<br/>git context, referenced_paths,<br/>recent_transcript (Claude only)"]
  F1 --> G{"LLM (default: Haiku) judges<br/>via structured output"}
  F2 --> G
  G -->|allow| H["Run"]
  G -->|deny| I["Refuse with deny_message"]
  G -->|fallthrough| J["Back to the upstream prompt"]
Loading

What ccgate puts in front of the LLM (representative fields):

  • tool_name, tool_input, and tool_input_raw (the original JSON payload, passed through verbatim).
  • cwd, repo_root, branch_name, and worktree info from gitutil.Context. The working-tree dirty/clean state is not delivered.
  • referenced_paths — paths extracted from tool_input on a best-effort basis. Supported tools: Read, Write, Edit, MultiEdit, Glob, Grep, Bash. For apply_patch (Codex) and MCP tools, referenced_paths is empty; the LLM reads tool_input_raw directly to see hunk targets or call arguments.
  • Claude-only: permission_mode (switches the prompt to plan-mode rules when "plan"), permission_suggestions, recent_transcript, and settings_permissions (treated as a hint, not a whitelist).

For the complete input list per target, see docs/claude-code.md and docs/codex-cli.md.

Configuration

Config file loading order

Order Claude Code Codex CLI
1 Embedded defaults (always applied as the base) Embedded defaults
2 ~/.claude/ccgate.jsonnet (global) ~/.codex/ccgate.jsonnet
3 {main_worktree}/.claude/ccgate.local.jsonnet (linked worktree only, untracked-only) {main_worktree}/.codex/ccgate.local.jsonnet
4 {repo_root}/.claude/ccgate.local.jsonnet (untracked-only) {repo_root}/.codex/ccgate.local.jsonnet

Merge rules at a glance:

  • Lists (allow / deny / environment) — a layer that sets the field replaces the carried-over list. append_* appends instead.
  • Scalars (log_* / metrics_* / fallthrough_strategy) — per-field overwrite.
  • provider block — replaced atomically as a unit (no per-field merge).

Project-local configs are loaded only when not tracked by Git. disable_load_main_worktree_local_config: true in layer (1) or (2) skips layer (3); it is ignored when written into (3) or (4).

Full merge details and the complete field reference are in docs/configuration.md.

Rule tuning

Once provider setup is done, this is the entry point for allow / deny / append_*.

  • Inspect defaults: ccgate claude init | less / ccgate codex init | less (-p writes a .local.jsonnet skeleton).
  • Where to put it: global ~/.<target>/ccgate.jsonnet, project-local <repo>/.<target>/ccgate.local.jsonnet (untracked-only).
  • Replace vs append: append_allow / append_deny / append_environment keep the embedded defaults and add your entries. allow: / deny: replaces the list wholesale (only your entries are in effect).

The full guide — rule-writing patterns for Claude / Codex (append_allow, append_deny, full replace), deny_message: hints, std.native('env') / must_env for env-derived values, the ccgate <target> metrics --details N iteration workflow — lives in docs/rule-tuning.md.

Providers and credentials

Switch providers by setting provider.name (and provider.model if needed) in any layer:

{
  provider: {
    name: 'openai',
    model: '<openai model name>',  // see docs/providers.md for model selection
  },
}

Export the matching API key — see docs/providers.md#api-keys. If the key is missing, ccgate falls through to the upstream tool's permission prompt, so flipping providers cannot break the hook.

Provider switching, model selection constraints, API key resolution order, and compatible-proxy setup are all consolidated in docs/providers.md.

Refreshable credentials (AWS STS, Vertex ADC, OpenAI-compatible gateways with virtual keys, internal key brokers — anything a static env var cannot keep up with) are handled via provider.auth. Three shapes are supported:

  • type=exec — ccgate runs a credential helper command and uses its stdout as the credential, with caching keyed on expires_at.
  • type=file — ccgate reads a credential file written by an external rotator.
  • type=profile — Anthropic-only; ccgate hands an ant auth login profile to the SDK and the SDK refresh-token loop owns the credential.

The full helper contract, caching, 401/403 behaviour, and the recovery checklist live in docs/api-key-helper.md.

Fallthrough strategy

When the LLM is not confident enough to decide, ccgate returns fallthrough and the AI tool shows its interactive permission prompt. That is fine in a human-in-the-loop session, but blocks unattended runs. Set fallthrough_strategy to force a fixed verdict on LLM uncertainty (default is ask):

{ fallthrough_strategy: 'deny' }  // Safer; recommended for anything unattended.

allow auto-approves operations the LLM itself was unsure about, so use it sparingly. The full value semantics, what is not covered (truncated API responses, missing keys, bypassPermissions / dontAsk, user-interaction tools, etc.), and the metrics audit columns are in docs/configuration.md.

Logging and metrics

  • Logs: $XDG_STATE_HOME/ccgate/<target>/ccgate.log (or ~/.local/state/ccgate/<target>/ when unset).
  • Metrics: $XDG_STATE_HOME/ccgate/<target>/metrics.jsonl.
  • Both rotate on a configurable _max_size threshold.
ccgate claude metrics                 # last 7 days, TTY table
ccgate claude metrics --details 5     # drill into the top-5 fallthrough / deny commands
ccgate codex  metrics --json          # machine-readable output

Column meanings, the JSON entry schema, and the credential-failure aggregation are in docs/configuration.md#metrics-output.

Known limitations

  • Plan mode correctness is prompt-only (Claude only). Under permission_mode == "plan", ccgate relies on the LLM plus prose in the system prompt to (a) reject implementation-side writes and (b) allow read-only queries without requiring an allow-guidance match. Either side can misfire.
  • No surgical reset for a single embedded default rule. A layer can either replace a list wholesale (allow: [...]) or append to it (append_allow: [...]). Removing one specific embedded allow / deny rule while keeping the rest requires re-stating the whole list minus that one entry.
  • No runtime conditional logic in jsonnet. jsonnet evaluation happens once per hook invocation, at config-load time, before ccgate sees tool_input. Rules cannot branch on tool_input / git working-tree state / external command output. Runtime classification is the LLM's job. Config-time env reads via std.native('env')(name) / std.native('must_env')(name) are available for things like embedding a host name into a rule string.

Documentation

CLI reference

ccgate                                       Read HookInput JSON from stdin (Claude Code hook). Equivalent to `ccgate claude`.
ccgate claude                                Same as bare ccgate, explicit form (recommended for new users).
ccgate claude init [-p] [-o FILE] [-f]       Output the embedded Claude Code defaults.
ccgate claude metrics [...]                  Show Claude Code usage metrics.
ccgate codex                                 Read HookInput JSON from stdin (Codex CLI hook).
ccgate codex init [-p] [-o FILE] [-f]        Output the embedded Codex CLI defaults.
ccgate codex metrics [...]                   Show Codex CLI usage metrics.

Top-level ccgate init and ccgate metrics are not real subcommands — they print a one-line pointer to the per-target form and exit 2.

Development

mise run build    # Build binary
mise run test     # Run tests
mise run vet      # Run go vet
mise run schema   # Regenerate schemas/{claude,codex}.schema.json

Nix (flakes)

A flake.nix is provided as an alternative to mise. It pins Go 1.25, golangci-lint, gopls, goimports, staticcheck, and delve into a dev shell.

nix develop                    # Enter the dev shell
nix develop -c go test ./...   # Run a one-off command in the shell

Requires Nix with flakes enabled (experimental-features = nix-command flakes).

macOS note: path_helper may shadow Nix paths with system bins — verify with which go.

Articles

License

MIT

About

LLM-powered PermissionRequest hook for coding agents (e.g. Claude Code)

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors