Skip to content

Repository files navigation

gatekit

A small, generic framework for a "focus gate" hook: something that blocks a prompt (or any other action) while independent, decoupled checks have something outstanding. gatekit itself knows nothing about what those checks are -- it only merges and renders JSON files that modules write, on whatever schedule, in whatever language, from whatever repo they live in.

Escape valves (read this first)

Three ways out, from broadest to narrowest:

  • Kill switch -- unconditional, no rate limit. gatekit off stops gating entirely until you gatekit on. gatekit off 1h stops it for a fixed duration instead of indefinitely. This is checked before anything else in the engine runs, so a broken module, a corrupt state file, or a bug in gatekit's own tiering logic can never stand between you and this working. If the binary itself is ever broken, touch ~/.cache/gatekit/state/disabled by hand does the same thing -- no parsing required.
  • Snooze -- rate limited, scoped. gatekit snooze 2h snapshots whichever items are overdue right now and holds exactly those for two hours. Anything not in that snapshot -- including an item that comes due later -- still blocks normally. gatekit unsnooze ends it early.
  • Bypass -- rate limited, one-shot. gatekit bypass waves through whatever's currently overdue, once, for the current invocation only.

Snooze and bypass share one rate-limit budget (bypass_rate_limit per bypass_window, with a bypass_cooldown between uses -- 2/hour and 5 minutes by default). The kill switch shares nothing with them and has no limit of its own, on purpose: a rate-limited escape hatch that runs out is not really an escape hatch, and gatekit's own design started from watching that happen to someone using an earlier, single-purpose version of this idea.

Reload: ask modules to catch up

gatekit reload is not an escape valve -- it never lets an overdue item through. It asks every module to re-poll its own upstream data right now, instead of waiting out its own schedule, for the case where you've already resolved something (replied, closed the ticket, whatever) and don't want to wait for the next scheduled tick to see it drop off.

gatekit itself has nothing to reload; Evaluate already reads sources/ fresh on every call. This writes a marker file and nothing more -- responding to it is opt-in per module (see docs/module-authoring.md). A module that never checks loses nothing: it catches up on its own next run either way, and until then gatekit status/a block message reports a warning naming which module hasn't refreshed since the request.

Install

go install github.com/312-dev/gatekit/cmd/gatekit@latest
go install github.com/312-dev/gatekit/cmd/gatekit-stalefiles@latest  # the reference module, optional

Or grab a prebuilt binary from GitHub Releases if you don't have a Go toolchain handy.

How it fits together

sources/*.json   <- module-owned; gatekit never writes here, only reads
      |
      v
   gatekit        <- pure library: tiering, degrade policy, snooze, bypass,
                      kill switch. No network calls, no domain knowledge.
      |
      +--> adapter/claudecode   Claude Code's UserPromptSubmit block JSON
      +--> adapter/exitcode     the portable exit 0/2 + stderr contract

cmd/gatekit is the installed binary. gatekit hook runs the portable exit-code adapter; gatekit cc-hook runs the Claude Code adapter. Point either one at your hook runtime of choice.

Register a module

A module is anything that writes one JSON file to sources/<module>.json, atomically (temp file + rename), on its own schedule. gatekit polls nothing and calls nothing -- it just reads that directory every time it evaluates. See docs/module-authoring.md for the full contract, and cmd/gatekit-stalefiles for a complete, working example (it flags files in a watched directory older than N days -- deliberately trivial, included only to prove the contract end to end).

Wire your module in however fits your platform -- a systemd timer, a cron entry, a long-running daemon's own loop. gatekit doesn't care.

Three tiers, not a binary block/allow

Each item is in exactly one of these -- mutually exclusive states, not slices of one ranked list:

  • pending -- before the item's sla_seconds grace period has elapsed. Never blocks. Shown with a countdown to when it becomes overdue.
  • held -- due, but excused: either an active snooze names it, or its source is degraded under on_degraded: "open". Visible, doesn't hold anything up.
  • overdue -- due, with nothing excusing it. Every overdue item blocks at once; there's no cap and no front-N. Whether something's due is decided entirely by the module, through first_seen_at plus sla_seconds -- gatekit itself never ranks or rations.

Health and degrade policy

Each source file's freshness (engine-measured mtime) and its own self-reported ok field combine into a health state. A module that goes stale or reports ok: false defaults to fail-open (on_degraded: "open"): its due items are held instead of blocking. A module can opt into on_degraded: "closed" if not knowing is itself the problem it exists to report -- in that case a degraded module's due items still block, always with a warning attached explaining why.

Configuration

~/.config/gatekit/config.json (or $GATEKIT_CONFIG):

{
  "stale_after": "15m",
  "unknown_after": "2h",
  "module_policies": {
    "slow-cron-module": { "stale_after": "6h" }
  },
  "bypass_rate_limit": 2,
  "bypass_window": "1h",
  "bypass_cooldown": "5m"
}

sources/ and state/ default to ~/.cache/gatekit/{sources,state}, overridable via $GATEKIT_SOURCES_DIR / $GATEKIT_STATE_DIR.

License

MIT -- see LICENSE.

About

A generic, portable focus-gate hook framework: merges JSON files from independent modules into a tiered block/allow decision.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages