Chock.sh Build your plugin

Guides

Troubleshooting

Measured problems and fixes, checking hooks with chock doctor, reading the decision log, install notes per agent, what happens when a guard cannot decide, and arming a fresh clone.

This page starts with problems a person measured, and their fixes. Then it shows how to check that each agent's hooks are in place and running, and how to read what they decided. Last come each agent's install notes as the engine prints them, and the engine docs on the cases that most often surprise people.

Measured problems and fixes

Windows: a Claude Code marketplace update fails with an SSL certificate error

Measured on 2026-10-12, on Windows with antivirus that intercepts HTTPS: Claude Code's marketplace update failed with SSL certificate problem: unable to get local issuer certificate, while a plain git fetch in the same shell succeeded. What fixed it: git config --global http.sslBackend schannel, then cloning the stale marketplace copy again. The chock plugin for Claude Code was not affected: it installs from a local folder marketplace, with no git fetch.

Check the hooks: chock doctor

In most clients, if a hook crashes, times out or never loads, the action goes through anyway. So a silent hook looks exactly like a protected session. chock doctor checks that each client's chock hooks are in place, current, and seen running, both in repo hooks and in the plugin. It only reads. It uses no network and never runs a client. Each check prints one line, PASS, WARN, FAIL or INFO, with a one-line fix:

  • Hooks: the file the client reads runs chock's runtime, and the runtime is there.
  • Launcher: the launcher is there, and a python3, python or py on PATH is 3.11+.
  • Engine: the runtime matches, byte for byte, what the installed chock builds. For a plugin, doctor also compares the engine version and commit the plugin recorded.
  • Trust: some clients (Codex, Grok) run a hook only after a person trusts it. Doctor shows how to do that. Chock cannot read the client's record of it.
  • Client: the client versions agentseam has seen working in a live run. --client-version compares yours with them.
  • Last fired and heartbeat: from the decision log, the last record per hook point, and the session_start heartbeats. A client with no documented session-start event reads "no heartbeat available", never a failure.

It exits 1 when any check fails or it finds no chock hooks, else 0.

chock doctor

Check each client's hooks on each route: present, current, and seen running (--client X)

usage: chock doctor [-h] [--repo REPO] [--client CLIENT] [--client-version CLIENT_VERSION]
                    [--sessions SESSIONS] [--json]

`chock doctor [--client X] [--repo .]`: each client's hooks on each route, PASS/WARN/FAIL with a
fix.

options:
  -h, --help            show this help message and exit
  --repo REPO           the repository whose `chock sync` hooks to check
  --client CLIENT       one client: an install client id (claude-code) or agent id (claude_code)
  --client-version CLIENT_VERSION
                        the client's version, compared with what agentseam witnessed
  --sessions SESSIONS   sessions the heartbeat check covers
  --json                one JSON object per target

The engine's doctor docs

Read what the hooks decided

The decision log (see Monitor) is where to look when a policy did not fire, or fired when you did not expect it. What the engine docs say about reading it:

  • Run chock doctor first. In most clients, if a hook crashes or never loads, the action goes through anyway. So a session with no session_start heartbeat is the sign of a hook that did not run, and doctor reports it.
  • chock status --only log lists the decisions from both install methods. At the default level, info, chock writes only blocks, asks, warns, guard faults and overrides. It does not write allows.
  • To see every call, allows included, with a redacted trace (wire event, tool, matched rule, the guard's exit code, per-stage timings), a person sets the level to debug: CHOCK_LOG_LEVEL=debug in the shell the agent client was started from, or {"level": "debug"} in .chock/chock-log.json. An agent cannot change the level.
  • chock status --summary lists the top guard faults and warns when a hook's p95 is within 25% of its client's hook timeout, because a call that runs past that timeout goes through.
  • Logging never changes a verdict, never raises and never blocks. If chock cannot write a line, it drops the line silently.
  • chock witness export --client <client> writes that client's records, redacted, to a new local file and prints the agentseam issue to attach it to. It uploads nothing.

Install notes per agent

Claude Code (claude-code)

Both commands are safe to repeat. The plugin loads in place from the directory: restart the Claude Code session, or run /reload-plugins in it, to pick up a rebuild.

Cursor (cursor)

Cursor loads ~/.cursor/plugins/local/chock-guardrails as a local plugin: run "Developer: Reload Window" (or restart Cursor), then check it under Customize. There is no command to run.

Codex (codex)

Then install chock-guardrails from the chock-local marketplace in /plugins, and review and trust its hooks in /hooks: until you do, they do not run.

GitHub Copilot (copilot)

VS Code finds plugins the Copilot CLI installed. Without the CLI, add the plugin folder to your VS Code user settings yourself:

Devin (devin)

On Windows, devin plugins install --local creates a symbolic link, so it needs Developer Mode (Settings > System > For developers) or an elevated shell; otherwise it fails with "os error 1314: A required privilege is not held by the client".

The plugin is linked in place: a rebuild applies from the next Devin session.

Grok (grok)

Plugins are off in grok until enabled, and chock does not edit grok's config: run grok plugin list for the name grok gave this plugin (chock expects chock-guardrails, its folder name; the docs do not say how a plugin with no plugin.json is named), then grok plugin enable <name>, or add it to enabled under [plugins] in ~/.grok/config.toml, or press Space on it in the Plugins tab. Re-run the install command after a rebuild.

goose (goose)

goose discovers ~/.agents/plugins/chock-guardrails as a user plugin, in every project: start a new goose session. There is no command to run (goose plugin install takes a git URL only). To turn it off, add chock-guardrails to disabledPlugins in ~/.config/goose/settings.json.

Antigravity (antigravity)

Antigravity 2.0 and the IDE read plugin folders from ~/.gemini/config/plugins/ instead: copy ~/.chock/marketplace/antigravity/chock-guardrails there. Its hooks name ~/.chock/marketplace/antigravity/chock-guardrails by absolute path (Antigravity documents no plugin-root variable), so keep that folder where it is; re-run the install command after a rebuild.

OpenHands (openhands)

OpenHands loads a local plugin when it starts: run openhands --plugin ~/.chock/marketplace/openhands/chock-guardrails, or add "~/.chock/marketplace/openhands/chock-guardrails" to sources under [plugins] in ~/.openhands/config.toml (chock does not edit it). Its hooks name ~/.chock/marketplace/openhands/chock-guardrails by absolute path (OpenHands documents no plugin-root variable), so keep that folder where it is; a rebuild applies from the next start.

Kimi Code (kimi-code)

Kimi Code installs plugins from its TUI only, and chock does not drive it: in Kimi Code run /plugins install ~/.chock/marketplace/kimi-code/chock-guardrails, then /reload (or /new). Kimi copies the plugin to $KIMI_CODE_HOME/plugins/managed/chock-guardrails/ and runs that copy, so after a rebuild install it again. The plugin applies to every project you open (Kimi has no project-level plugin scope); /plugins disable chock-guardrails turns it off.

Factory Droid (factory-droid)

Run these from the root of the project the plugin is for: --scope project records it in that project's Droid settings. The marketplace is local to this machine; chock publishes nothing.

Droid names a local marketplace from its source; if droid plugin marketplace list shows a name other than chock-local (a --dest folder named otherwise), install chock-guardrails@<that name>. Droid copies the plugin into its cache: after a rebuild run droid plugin marketplace update and droid plugin update chock-guardrails@chock-local --scope project, then start a new session.

From the engine docs: docs/enforcement-surfaces.md

What happens when the guard cannot decide

The levels above grade the outer boundary: what the client does when chock's hook never runs or dies outright. There is an inner one too — what the hook says when it did run and could not reach a verdict — and the two answers are not the same. gate/guard_runner.py distinguishes six such causes and answers only one of them with an allow.

CauseWhat chock returnsWhy
The command will not tokenize (unbalanced quotes, trailing backslash, PowerShell quoting)the guard's own verdictThe guard still runs: argv is a whitespace split (CHOCK_ARGV_FALLBACK=1) and CHOCK_RAW_COMMAND carries the exact string. rm -rf / #' is valid bash and invalid shlex, so a guard that reads the raw command must get the chance to refuse it.
The command is empty after tokenizingallowThere is nothing to check.
No bash on the machine can resolve the guardaskNo guard ran. The prompt names the fix: install Git for Windows (it ships bash), or put bash on PATH.
The guard the hook names is not on diskdenyThe hook config names it, so its absence is a broken install, not nothing to check. The reason says to run chock sync --repo ..
The guard crashed, exited a code that is none of 0, 1 or 3, or exited 1 with no reason or a traceback / syntax erroraskThe control was installed, reachable and runnable, and still produced no answer. (Exit 3 is not this: it is the guard asking on purpose, and its own first line is the prompt. Exit 1 blocks only with a reason on stderr, as spec/script-backed-gates.md states.)
The guard hit its 30-second timeoutaskSame: the control ran and did not decide.

A guard that fails refuses or asks; it never reports an allow it never established.

The engine's time limits always fire before the client's. A client's hook timeout fails open (the command runs); the engine's own limits ask a person or deny. So chock keeps one budget, ENGINE_BUDGET_SECONDS (30 s, gate/budget.py), per hook invocation: the bash probes, git status, the guard, the gate runner and a tool-call script all draw on the same deadline, and each subprocess gets only what is left. A git status that does not finish at the turn's end is a refusal, never a clean worktree. The timeout written into the client's hook config is that budget plus a 15 s interpreter start-up margin (TIMEOUT_SECONDS, 45 s), in each client's own unit.

SurfaceClientsTimeout written
In-agent (chock sync: .claude/, .cursor/, .codex/, .github/hooks/, ...)Claude Code, Codex, Copilot, Cursor, Devin, Antigravity, Gemini CLI, Grok, Tabnineyes, every hook entry
In-agentWindsurfno key: its documented hook entry takes command, powershell, show_output and working_directory only, so its own default (not documented) applies
Plugin package (hooks.json)Claude Code, Codex, Copilot, Cursor, Devin, goose, Grok, Antigravity, OpenHands, Kimi Code, Factory Droidyes, every hook entry
ClientFieldUnitWrittenClient default when the key is absent
Claude Code, Codex, Devin, Antigravity, Grok, Kimi Code (in kimi.plugin.json), Factory Droidtimeoutseconds45Codex 600 s; Devin 600 s (command hooks); Antigravity 30 s; Grok 5 s; Kimi Code 30 s; Factory Droid 60 s
Gemini CLI, Tabninetimeoutmilliseconds4500060000 (60 s) both
Copilottimeout and timeoutSecseconds45not checked
Cursortimeout (with failClosed)seconds45not checked
Windsurfnonen/an/aundocumented

A default below 45 s is exactly the hole this closes: Grok's 5 s and Antigravity's and Kimi Code's 30 s would end the hook before the engine's own 30 s limit and the command would run unchecked. tests/test_hook_timeout_budget.py walks every emitted hooks document and fails on a hook entry that lacks a timeout above the budget, unless its client is in the no-key allow-list (NO_TIMEOUT_KEY_VENDORS). Sources are in src/chock/data/hook_timeouts.json.

What an ask becomes depends on the client, and no client turns it into a silent allow.

ClientGate chock installsAn ask on the wire
Claude CodePreToolUse, via the settings fragment install-hooks mergespermissionDecision: "ask" — prompts the user to confirm
VS Code agent mode / Copilot CLIPreToolUse, via the agent-hooks filepermissionDecision: "ask" — forces a confirmation, and overrides the client's own auto-approve
CursorbeforeShellExecution, via the Cursor hooks file{"permission": "ask"} — honoured at this gate. Cursor's generic preToolUse accepts the value but does not enforce it, which is one reason chock installs the shell gate instead
Codex CLI (hand-wired only)PreToolUsedeny. Codex's own parser rejects ask as unsupported and then fails open on the response it rejected, so agentseam's adapter degrades it to a deny rather than emit a value that would silently permit the call. A guard broken on every command therefore blocks on Codex where the others prompt

Each row is cited to vendor source or vendor documentation at a named ref, so a reader can recheck it rather than take this table's word:

  • Claude Code — code.claude.com/docs/en/hooks, read 2026-08-31, PreToolUse hookSpecificOutput field table: ""ask" prompts the user to confirm." Claude Code is not open source, so this is documentation, not source.
  • VS Code agent mode — microsoft/vscode at 718038e170df9c66a15087cebda424d9c7f051ff, src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts. resolveAutoConfirmFromHook (:877-930) synthesises a confirmation and sets allowAutoConfirm: false; :622-627 carries the comment "A preToolUse hook that returned ask explicitly forces a confirmation, so never let preApproved override it."
  • Cursor — not verified from a vendor artifact. The claim rests on agentseam's recorded doc-basis verification (2026-08-26), which distinguishes preToolUse (accepts ask, does not enforce it) from beforeShellExecution (honours it). Second-hand, and labelled as such rather than presented as checked.
  • Codex CLI — openai/codex at 32f48598a0609a882e5847f0d3e35d6d67f375bc. codex-rs/hooks/src/engine/output_parser.rs:458-460 returns "PreToolUse hook returned unsupported permissionDecision:ask"; :144 computes a block reason only when nothing was rejected, and codex-rs/hooks/src/events/pre_tool_use.rs:234-244 sets should_block in the non-rejected arm alone — so a literal ask there would let the call through.

This raises no coverage grade. A control is only as strong as its worst degradation, and the empty command above still allows — so chock's in-agent controls stay at the level the ladder gives a control that degrades to allowing until that grade is re-derived deliberately.

From the engine docs: docs/adopting.md

How to verify a fresh clone enforces

After git clone in a fresh directory, the committed artifacts are present but the git hooks are not — .git/hooks/ is never cloned, so the repo enforces nothing until the hooks are reinstalled:

  1. Run chock sync --repo . to install the hooks from the committed artifacts.
  2. ls .chock/bin/gate.py should exist.
  3. ls .chock/compiled/scan-secrets/git-hook/gate.json should exist.
  4. Commit a file containing a credential. scan-secrets should block it and print the policy message.

chock init and chock sync write these un-ignore lines to your .gitignore, so a global ignore rule cannot keep the gate out of your commits. The bin/ in the Visual Studio, .NET and Java templates is the usual culprit: it matches .chock/bin/, and every agent hook then names a file a clone does not have. chock check reports a hook target that is missing or git-ignored. If your own .gitignore has a broad .chock/ rule, keep the lines below after it:

!.chock/bin/
!.chock/bin/**
!.chock/compiled/
!.chock/compiled/**

Built from engine 5a4b6287a46e and catalog 1e60d211c1bb.