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,pythonorpyon 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-versioncompares yours with them. - Last fired and heartbeat: from the decision log, the last record per hook point, and the
session_startheartbeats. 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
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 doctorfirst. In most clients, if a hook crashes or never loads, the action goes through anyway. So a session with nosession_startheartbeat is the sign of a hook that did not run, and doctor reports it. chock status --only loglists 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=debugin the shell the agent client was started from, or{"level": "debug"}in.chock/chock-log.json. An agent cannot change the level. chock status --summarylists 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.
| Cause | What chock returns | Why |
|---|---|---|
| The command will not tokenize (unbalanced quotes, trailing backslash, PowerShell quoting) | the guard's own verdict | The 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 tokenizing | allow | There is nothing to check. |
| No bash on the machine can resolve the guard | ask | No 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 disk | deny | The 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 error | ask | The 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 timeout | ask | Same: 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.
| Surface | Clients | Timeout written |
|---|---|---|
In-agent (chock sync: .claude/, .cursor/, .codex/, .github/hooks/, ...) | Claude Code, Codex, Copilot, Cursor, Devin, Antigravity, Gemini CLI, Grok, Tabnine | yes, every hook entry |
| In-agent | Windsurf | no 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 Droid | yes, every hook entry |
| Client | Field | Unit | Written | Client default when the key is absent |
|---|---|---|---|---|
Claude Code, Codex, Devin, Antigravity, Grok, Kimi Code (in kimi.plugin.json), Factory Droid | timeout | seconds | 45 | Codex 600 s; Devin 600 s (command hooks); Antigravity 30 s; Grok 5 s; Kimi Code 30 s; Factory Droid 60 s |
| Gemini CLI, Tabnine | timeout | milliseconds | 45000 | 60000 (60 s) both |
| Copilot | timeout and timeoutSec | seconds | 45 | not checked |
| Cursor | timeout (with failClosed) | seconds | 45 | not checked |
| Windsurf | none | n/a | n/a | undocumented |
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.
| Client | Gate chock installs | An ask on the wire |
|---|---|---|
| Claude Code | PreToolUse, via the settings fragment install-hooks merges | permissionDecision: "ask" — prompts the user to confirm |
| VS Code agent mode / Copilot CLI | PreToolUse, via the agent-hooks file | permissionDecision: "ask" — forces a confirmation, and overrides the client's own auto-approve |
| Cursor | beforeShellExecution, 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) | PreToolUse | deny. 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,PreToolUsehookSpecificOutputfield table: ""ask"prompts the user to confirm." Claude Code is not open source, so this is documentation, not source. - VS Code agent mode —
microsoft/vscodeat718038e170df9c66a15087cebda424d9c7f051ff,src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts.resolveAutoConfirmFromHook(:877-930) synthesises a confirmation and setsallowAutoConfirm: false;:622-627carries the comment "A preToolUse hook that returnedaskexplicitly forces a confirmation, so never letpreApprovedoverride it." - Cursor — not verified from a vendor artifact. The claim rests on agentseam's recorded doc-basis verification (2026-08-26), which distinguishes
preToolUse(acceptsask, does not enforce it) frombeforeShellExecution(honours it). Second-hand, and labelled as such rather than presented as checked. - Codex CLI —
openai/codexat32f48598a0609a882e5847f0d3e35d6d67f375bc.codex-rs/hooks/src/engine/output_parser.rs:458-460returns "PreToolUse hook returned unsupported permissionDecision:ask";:144computes a block reason only when nothing was rejected, andcodex-rs/hooks/src/events/pre_tool_use.rs:234-244setsshould_blockin the non-rejected arm alone — so a literalaskthere 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:
- Run
chock sync --repo .to install the hooks from the committed artifacts. ls .chock/bin/gate.pyshould exist.ls .chock/compiled/scan-secrets/git-hook/gate.jsonshould exist.- Commit a file containing a credential.
scan-secretsshould 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.