Skip to content

Latest commit

 

History

History
610 lines (515 loc) · 30.4 KB

File metadata and controls

610 lines (515 loc) · 30.4 KB

Pre-Commit Hooks

This bundle provides ETHOS-oriented Git hooks through the bundled Go runner. The bundle supports two layouts:

  • Source repo: pre-commit/
  • Vendored/submodule repo: coding-ethos/pre-commit/

The hook runners resolve either layout automatically. Installed Git hooks call the Go runner directly.

Install

Run from the bundle repo root:

make install-hooks
make cutover-install
make cutover-verify

In a consuming repo, run the same target from coding-ethos/.

When coding-ethos/ is a submodule, the root Makefile resolves the parent repo automatically and installs hooks into the parent repo's .git/hooks.

Before the hook entrypoints are installed, make install-hooks also generates the consumer repo's pyrightconfig.json, mypy.ini, ruff.toml, .pylintrc, .yamllint.yml, .bandit.yml, .sqlfluff, tombi.toml, .golangci.yml, and .coding-ethos/gemini/prompt-pack.json from the shared bundle inputs plus any consuming-repo overrides.

make install-hooks installs .git/hooks/pre-commit, pre-push, and commit-msg as small executable scripts that call bin/coding-ethos-run git-hook <hook> with the original Git arguments. The runner repairs missing checkout-local runtime artifacts with make build and dispatches to the built hook binary under coding-ethos/bin/.

make cutover-install installs the Git hook entrypoints, syncs Claude, Codex, and Gemini repo-local agent hook settings, and then verifies the full cutover surface. make cutover-verify checks the installed Git hook entrypoints, runs agent-hooks verify, verifies required runtime ignores through the compiled repo.required_ignores policy, runs the policy runtime validation hook, and prints a concise TOON readiness report. Blocked reports include fix_first entries that name the stale or missing surface and the next action.

Each top-level hook runner invocation logs stdout, stderr, and run metadata under .coding-ethos/hook-runs/<run-id>/ in the repo being checked. Keep generated runtime subpaths such as .coding-ethos/hook-runs/, .coding-ethos/lint-runs/ and .coding-ethos/cache/ ignored in both the bundle repo and consuming repos; do not ignore the parent .coding-ethos/ directory, because .coding-ethos/memories/ is repo source. The cutover gate reports missing or overly broad ignore rules before installation, and normal hook execution still fails before writing logs when required runtime paths are not ignored.

Required tools:

  • go 1.26 or newer
  • uv

make build installs required checkout-local managed tools under build/toolchain/. shfmt and golangci-lint are built into build/toolchain/go-bin/; ShellCheck, actionlint, hadolint, and dotenv-linter are installed from pinned GitHub release assets into build/toolchain/github-bin/. The compiled coding-ethos-toolchain install-managed-toolchain command owns manifest parsing, install-skip decisions, Go/Rust/GitHub installer dispatch, digest verification, archive extraction, and installed-manifest writes. build/toolchain/manifest.tsv records the installed toolchain. Hook execution prepends managed tool directories to PATH, and Go hook commands resolve managed absolute paths; host-global linter installs are not a runtime contract.

Agent shell remediation uses the short cerun --rewrite -- <command> binary. cerun delegates to coding-ethos-run agent-shell --rewrite -- <command> so blocked providers can resubmit unmanaged shell commands through the coding-ethos runtime boundary and command rewrite rules without copying a long command prefix. Use cerun --check -- <command> to run the same policy preflight without executing the command. The runner records preflight and execution attempts in .coding-ethos/code-intel.duckdb, carries --intent <text> or the CODING_ETHOS_STRATEGIC_INTENT/Gemini update-topic environment into sandbox evidence and CEL event facts, emits SARIF agent_remediation for blocked runner commands, and blocks command argv that appears to contain secrets or local-machine paths before host execution.

cerun git <args> and cerun python <args> are short forms for cerun --rewrite -- git <args> and cerun --rewrite -- python <args>; cerun lint <args> dispatches to the managed policy-lint path. Recursive runner invocations are rejected at the runtime edge, so the accepted shape is a single cerun boundary around the real target command.

Claude Bash hooks must not emulate file tools with shell commands. cat, sed, awk, tee, and echo/printf write redirection are blocked in Bash events so file reads and edits stay on provider file tools where hooks receive structured file paths. Claude /permissions allowlists should grant only the literal runner boundary, for example Bash(cerun -- *) and Bash(cerun --check -- *); direct Git, absolute Git, shell file tools, nested runner commands, and inline environment preludes remain policy violations even when a provider permission prompt would otherwise allow Bash.

Provider-owned memory and permission request systems are integration inputs, not alternate enforcement paths. coding-ethos records blocked command context, SARIF remediation, active TodoWrite context, strategic intent, runtime duration, and code-intelligence events for provider memory/MCP consumers, while the hook and runner boundary remain the fail-closed enforcement floor.

Run

From the bundle repo root:

make doctor
make pre-commit
make pre-commit-all
make pre-push
make validate
make hook-plan

Run a single group directly:

cd "$(git rev-parse --show-toplevel)"
bin/coding-ethos-run run-group python-static path/to/file.py
bin/coding-ethos-run run-group syntax path/to/config.yaml

Run commit-message checks directly:

tmp="$(mktemp)"
printf 'feat(hooks): update bundle\n' > "$tmp"
make commit-msg MSG="$tmp"
rm -f "$tmp"

Hook bypass is forbidden. Do not use --no-verify.

Layout

Primary files:

  • ../Makefile - root-level hook entry points and Git hook installation
  • ../config.yaml - repo-root bundle policy and per-check defaults
  • ../pyrightconfig.json, ../mypy.ini, ../ruff.toml, ../.pylintrc, ../.yamllint.yml, ../.bandit.yml, ../.sqlfluff, ../tombi.toml, ../.golangci.yml - generated consumer-repo tool configs
  • ../.coding-ethos/gemini/prompt-pack.json - generated consumer-repo Gemini prompt pack with rendered prompts and per-check runtime metadata
  • hooks/pyproject.toml - Ruff, mypy, pyright, and tool dependency config for the hook project
  • ../bin/coding-ethos-run - compiled hook/runtime entrypoint; policy metadata validation, managed GitHub asset installation, cutover reporting, and Git hook entrypoint installation/verification are delegated to compiled Go helpers
  • go/cmd/coding-ethos-hook-runner/main.go - Go-backed hook commands, including the active Gemini AI review runner

The active Go Gemini runner now executes file batches concurrently, applies repo-local response caching under .coding-ethos/cache/, supports per-check model_overrides and service_tier_overrides, reuses Gemini cachedContents entries when the same batch corpus is reviewed by multiple prompts, and can run standard, flex, or priority requests from merged config.yaml plus repo_config.yaml.

The hook runtime is built into the checked-out coding-ethos repository:

  • bin/ contains built hook and policy binaries.
  • build/policy/ contains the compiled policy bundle and source-hash metadata.

The old .git/coding-ethos-hooks/ runtime cache is legacy. The current runtime model is documented in docs/HOOK_RUNTIME_BOOTSTRAP.md: installed consumer hooks are generated runner entrypoint scripts; all repo-discovery, build-repair, and dispatch behavior lives in compiled Go while binaries and compiled runtime files are built and executed from the checked-out coding-ethos repository.

The same wrapper also exposes local policy-runtime entrypoints:

bin/coding-ethos-run agent-hook
bin/coding-ethos-run agent-hooks print
bin/coding-ethos-run agent-hooks sync
bin/coding-ethos-run agent-hooks doctor
bin/coding-ethos-run agent-hooks verify
bin/coding-ethos-run cutover install
bin/coding-ethos-run cutover verify
bin/coding-ethos-run policy-lint --staged
bin/coding-ethos-run policy-git --check-only commit -m test
bin/coding-ethos-run hook-log-analyze
bin/coding-ethos-run hook-log-summary

agent-hook reads agent hook JSON from stdin and never calls Gemini. Gemini checks stay in the Git hook stages: changed-file review on pre-commit and full review on pre-push.

agent-hooks print|sync|doctor|verify always covers every supported agent surface. There is no single-agent generation path because partial protection is not a valid install state. Claude output uses Claude Code's native hooks map. Claude MCP output writes project .mcp.json with a coding-ethos stdio server. Codex output enables [features].hooks in .codex/config.toml, writes managed native [hooks] entries and [mcp_servers.coding-ethos] in that same TOML file, and removes stale .codex/hooks.json. Gemini output writes native .gemini/settings.json hooks with hooksConfig.enabled = true and mcpServers.coding-ethos. doctor verifies those native activation files and fails when a provider does not point at the expected hook or MCP command. Codex hook generation uses one native command hook per supported lifecycle event, while the runtime normalizes aliases such as exec_command, run_shell_command, shell, write_file, and apply_patch; shell and edit policy does not depend on a single Codex tool name spelling. Codex settings are generated without inline PATH= mutation, use explicit shell/edit matchers for tool hooks, and use one matcher-free command hook for supported lifecycle events. A nested checkout only enforces the Codex hook whose consumer root is the nearest repo root, preventing parent and nested repos from both reporting the same tool event. verify executes provider-shaped runtime probes through the configured hook command after doctor succeeds. It proves the settings point at a runnable policy path; it is not a substitute for a real provider binary executing a live tool call.

agent-hook accepts each provider's supported event shape and normalizes it before policy evaluation. Claude may send native hook_event_name, tool_name, tool_input, and tool_response fields. Codex and Gemini CLI callers should send the provider-neutral shape:

{
  "provider": "gemini-cli",
  "event": "PreToolUse",
  "tool": "Bash",
  "input": {"command": "git status"}
}

The decoder also accepts camelCase hook fields (hookEventName, toolName, toolInput, toolResponse, exitCode), Gemini's BeforeTool, run_shell_command, and write_file names, Codex shell aliases (exec_command, run_command, run_shell, run_shell_command, shell, shell_command), and nested Codex-style tool_call.name plus tool_call.arguments. Provider identity does not weaken policy: the same git wrapper, filesystem, Python-edit, continuation, and post-tool output rules apply wherever the provider exposes the corresponding lifecycle hook.

Tamper and bypass blocks are intentionally louder than normal lint findings. Direct attempts to inspect, delete, rebuild, replace, chmod, or write managed hook binaries under coding-ethos/bin/ are treated as employment violations. Agent-facing output starts with a uniform CODING-ETHOS EMPLOYMENT VIOLATION warning before the policy-specific message, states that the actor has done something wrong, and warns that continued circumvention attempts may result in termination.

Hook responses are provider-aware. Claude keeps the full hookSpecificOutput contract, including updatedInput for transparent git-wrapper rewrites. Codex does not currently support updatedInput, so coding-ethos returns native block output (decision: "block" plus permissionDecision: "deny" for PreToolUse) when a raw git command must be rerun through the wrapper. Codex receives compact native additionalContext for supported lifecycle and post-tool advice, while blocked tool calls use compact reason text suitable for exit-code-2 stderr. Codex uses compact systemMessage only for supported events that do not expose additionalContext. Gemini uses native decision: "deny" and systemMessage for tool blocks, and maps AfterTool to the same internal PostToolUse feedback path for shell and edit advice. Agent-facing post-tool context replaces absolute repo, home, and temp paths with stable tokens, collapses multiline commands, and renders hook output as TOON line tables instead of escaped newline cells.

Every logged hook run is captured by the managed coding-ethos-hook-log Go tool. It creates .coding-ethos/hook-runs/<run-id>/metadata.env plus stdout and stderr logs. Adding --coding-ethos-debug to a hook-runner or Bash tool command strips that external flag before execution, enables structured debug logging for the run, and writes debug events to both debug.log and stderr. Agent-hook runs also write sanitized event.json diagnostics containing provider, event, tool, cwd, referenced files, command preview and hash, policy IDs, status, and output shape. The trace is meant for debugging policy routing and provider adaptation; it intentionally avoids dumping raw tool input.

Post-edit feedback for Write, Edit, and MultiEdit includes focused context, language-specific advice, compiled lint findings for the edited files, and a fast Ruff probe for Python files when ruff is available. Expensive external tool suites still belong to the Git hook/check path. Fast Ruff findings use the same ETHOS evidence maps as captured lint output, so known codes carry policy-grounded repair advice in post-edit context. When captured lint history has relevant failures for the same file area, post-edit feedback also surfaces recurring checks, recurring tool/code pairs, and unmapped tool/code pairs that still need ETHOS evidence-map coverage.

hook-log-summary summarizes .coding-ethos/hook-runs/ and hook-log-analyze ranks failed tools, codes, repeated findings, and output-quality problems such as raw output, escaped newline cells, or leaked absolute repo paths. The analyzer scans newest runs first and caps scanned runs plus examples so it stays interactive on large agent log directories. Both commands honor the same human, JSON, and TOON output selection as hook execution output. Compiled lint preflights also persist normalized result traces under .coding-ethos/lint-runs/. These are intended for offline trend analysis: which policies fail most often, which linter codes drive the most churn, and which ETHOS-backed advice should become more specific. Future guidance synthesis may use a very small LLM or local model over these traces, but hook-time enforcement remains deterministic and policy-bundle driven. Analyze those traces with:

bin/coding-ethos-run policy-lint --analyze-log
bin/coding-ethos-run policy-lint --analyze-log --for-files lib/python/app.py
bin/coding-ethos-run policy-lint --analyze-log --json
bin/coding-ethos-run policy-lint --replay .coding-ethos/lint-runs/<trace>.json

Analysis output uses the same human, JSON, and auto-selected TOON formats as other lint output, so agent callers get compact structured summaries by default. The analyzer reports top failing checks, top tool/code pairs, unmapped tool/code pairs, repeated file-policy patterns, ETHOS IDs, and deterministic guidance candidates. The --for-files filter narrows output to prior findings from the same file or same high-level file area so post-edit feedback can stay focused. Replay renders one saved normalized result without invoking the underlying linter, so malformed or low-quality agent output can be reproduced from the persisted trace. Direct agent lint runs are captured too. The agent hook rewrites common forms for ruff, mypy, pyright, pylint, shellcheck, golangci-lint, actionlint, yamllint, hadolint, bandit, sqlfluff, tombi, and dotenv-linter to the managed policy-tool <tool> wrapper when the provider supports command rewrites. This covers plain tool names, absolute tool paths, uv run <tool>, and python -m <tool> for Python-backed tools. The installed hook PATH also contains managed shims for tools that execute by name. Captured runs preserve exit codes while forcing machine-readable tool output, parsing diagnostics into the shared lint schema, enriching known findings with ETHOS evidence-map advice, writing normalized lint traces, and returning coding-ethos human or TOON output instead of raw linter output.

Raw Python execution follows the same repo-owned environment rule. The hook runtime prepends <repo>/.venv/bin after coding-ethos-managed directories so repo virtualenv tools are found after protected shims. Agent shell commands that invoke python, python3, or python3.x are rewritten, when possible, to uv run --project <repo> python ... for uv projects, or to <repo>/.venv/bin/python ... when only a virtualenv is present. Providers that cannot accept command rewrites are blocked with a message telling them to use the documented repo Python command.

Captured tool execution is intentionally controlled by coding-ethos. The target repo is an untrusted file tree and trace destination, not a source of trusted tool binaries, tool configuration, PATH, aliases, shell state, or uv project behavior. Python linters execute from the coding-ethos hook project via coding-ethos-managed versions and explicit generated config flags (ruff.toml, mypy.ini, pyrightconfig.json, .pylintrc, .yamllint.yml, .bandit.yml, .sqlfluff, and tombi.toml). Parent repo config files with matching names are ignored. Binary linters such as ShellCheck, actionlint, hadolint, dotenv-linter, and golangci-lint are installed by coding-ethos init into the managed runtime before they are trusted capture backends; host binaries are not a policy boundary.

Captured lint runs are treated as structured events. Each trace should preserve the original argv, rewritten argv, exit code, selected parser, parser outcome, normalized diagnostics, normalized findings, and a bounded redacted stdout/stderr excerpt when a tool fails before producing parseable diagnostics. Tool/config failures are first-class findings: a failed run with no parsed diagnostics must never render as findings[0].

Troubleshooting and regression coverage should work from the same contract:

  • replay a saved .coding-ethos/lint-runs/*.json trace without rerunning the underlying linter
  • maintain golden TOON output tests for normal findings, clean runs, invalid config, malformed JSON/text output, timeouts, and tool crashes
  • fail tests if blocked output contains empty finding tables, unredacted local paths, internal group/duration noise, or generic guidance without an actionable finding
  • keep parser failures visible as parser/tool failures instead of degrading to silent success or empty diagnostics

Configuration

Bundle defaults live in the coding-ethos repo-root config.yaml. Consuming repos can override them with one of these root-level files:

  • repo_config.yaml
  • repo_config.yml

You can also point the bundle at an explicit override file with CODE_ETHOS_PRECOMMIT_CONFIG.

Use bin/coding-ethos-run policy config-trace --json after enforcement config edits to validate known top-level sections, compile the merged policy bundle, and report policy/evidence/dispatch counts.

Legacy override names like coding-ethos.pre-commit.yaml are still accepted, but repo_config.yaml is the preferred consuming-repo entry point.

Set repo.protected_branch_work.enabled: false in repo_config.yaml when a consumer intentionally permits direct work on main/master. The switch disables branch-switch blocking and protected-branch file-write blocking together; it does not disable history-rewrite prevention. Amend commits, force pushes, branch-moving resets, git checkout -B, and git branch -f remain blocked; make a new commit instead.

License enforcement is intentionally not inherited from the bundle defaults. Consumers opt in with repo.license.spdx_identifier in repo_config.yaml; the compiler downloads that SPDX license text into the policy bundle, the hook verifies LICENSE without overwriting it, and source files must carry the configured SPDX license and copyright headers. The same compiled file-policy path also enforces configured PII scrub patterns and required runtime ignore paths such as .coding-ethos/hook-runs/.

Generated config drift is checked with:

make check-tool-configs
make check-gemini-prompts

Regenerate derived files with:

make sync-tool-configs
make sync-gemini-prompts

The Go runner invokes Python tools with uv run --project coding-ethos/pre-commit/hooks or uv run --project pre-commit/hooks. Ruff, mypy, pyright, pylint, yamllint, Bandit, SQLFluff, Tombi, and golangci-lint read policy from the generated consumer-repo config files at the repo root. The hook project hooks/pyproject.toml remains the isolated toolchain environment. Parent uv workspace membership is optional, not required.

Known external diagnostics can be enriched through policy.evidence_maps. Mapped findings keep their raw tool, code, location, severity, and message, then add policy ID, principle IDs, confidence, meaning, advice, and rerun commands. Unmapped diagnostics still flow through as ordinary lint findings. Agent-facing JSON, TOON, SARIF, provider-native block responses, hook traces, and lint traces derive a compact agent_remediation payload from that normalized result. The payload names a stable remediation ID, policy, ETHOS principle, skill, location or failed action, next steps, rerun commands, skill-loading instruction, and the MCP call an agent should use for deeper explanation. Agents with the full item can call remediation_explain. Type-checker evidence maps cover common mypy, Pyright, and Pylint findings for optional required dependencies, unknown type leakage, missing imports, unstable interfaces, and import cycles. Those findings point back to ETHOS guidance such as Protocol-first design, fail-fast required imports, and structural fixes instead of lazy imports or broad suppressions. When multiple tools report the same mapped policy at the same location, the lint result keeps one actionable finding and records the secondary tool/code in the finding detail. This keeps agent context focused on the repair instead of repeating equivalent diagnostics.

Important configurable areas:

  • style.* - shared cross-cutting settings like Python version and line length; style.python_version also drives pyupgrade, generated tool configs, and repo-root version consistency checks
  • python.source_paths, python.test_paths, python.stub_paths, python.extra_paths - shared repository layout inputs for generated tool configs
  • python.direct_imports - public-package import enforcement
  • python.util_centralization - banned direct utility imports and exemptions
  • python.sql_centralization - centralized SQL module name and exempt paths; test paths are exempt from SQL centralization enforcement
  • python.manifest_validation - candidate manifest paths and required sections
  • python.plan_completion - plan metadata filename, root markers, and done states
  • python.pytest_gate - banned markers and pytest command
  • python.file_docstrings - minimum sentence count and exempt filenames for file-level module docstrings
  • python.type_check - aggregated Ruff, mypy, pyright, and optional pylint command execution, hook-project execution, config injection, per-checker enablement, and excluded path fragments
  • python.docstring_coverage - native Tree-sitter docstring coverage threshold, path selection, exclude regexes, and ignore flags
  • hooks.* - normalized output format, agent environment detection, external tool timeout, severity thresholds, and canonical hook groups
  • tooling.pyright, tooling.mypy, tooling.ruff, tooling.pylint, tooling.yamllint, tooling.bandit, tooling.sqlfluff, tooling.tombi, tooling.golangci_lint - generated repo-root tool config defaults, including the expanded Go security, dependency, module-directive, modern-library, protobuf, test, and whitespace/style linter policy
  • gemini.* - AI review enablement, model, concurrency, timeout, repo context, and modal allowlist file patterns
  • go.* - compiled commitlint and commit attribution policy, text policy, line limits, and quiet-filter rules

Agent-facing hook feedback should render from normalized diagnostics instead of raw tool output. CODE_ETHOS_HOOK_OUTPUT_FORMAT=human|json|toon|auto controls structured hook reports; auto selects TOON when common agent caller environment markers are present and otherwise keeps the human terminal report. Verbose PostToolUse output is compressed before agent context injection by preserving command-opening lines, terminal failure lines, and an explicit omission marker instead of replaying repetitive middle output. Failed grouped hook runs emit a runner-owned execution summary before captured tool output, including group status, duration, failed groups, and per-command timing for in-process group execution. Go-owned policy checks, Python static checks, Gemini AI checks, docstring coverage, shellcheck/yamllint, and external analyzer orchestration use this normalized report path. Pylint config is generated as .pylintrc, but the Pylint checker is disabled by default in python.type_check.checkers; re-enable it per repo after the local .pylintrc policy has been reviewed. The canonical Go-owned hook groups are format, syntax, python-policy, python-quality, python-static, docs, security, shell, docker, workflow, go, and ai; compiled policy preflight owns commit message behavior. Run make hook-plan to print the active group and command plan.

For this repo, many project-specific checks are disabled by default because the codebase does not have SQL centralization, manifest, plan, or Go worktree requirements. Consuming repos enable and tune those checks in their override config.

Typical consuming-repo overrides include:

  • style.line_length for line-length policy shared across Ruff and yamllint
  • python.source_paths, python.test_paths, and python.stub_paths for nested layouts like lib/python/tests
  • python.direct_imports.packages for the repo's public package names
  • python.pytest_gate.test_command for nonstandard test roots like lib/python/tests
  • python.file_docstrings.min_sentences for stricter module-level docstring requirements
  • python.docstring_coverage.check_paths for nested source trees
  • python.docstring_coverage.ignore_private / ignore_nested_classes when a repo wants stricter coverage
  • python.type_check.excluded_path_fragments for generated or container-specific Python trees
  • python.sql_centralization and python.util_centralization for repo-specific wrapper modules
  • gemini.modal_allowlist_files for repo-configured file-level modal waivers instead of inline source comments

See ../repo_config.example.yaml for a minimal consumer-repo override file.

Policy lint selection can be inspected without running checks:

bin/coding-ethos-run policy-lint --scope staged --explain
bin/coding-ethos-run policy-lint --scope staged --explain --json

The explain output reports the selected policy checks, evaluator names, severity, ETHOS IDs, hook-owned tool selection, and the active evidence maps that turn external linter codes/messages into ETHOS-backed policy advice.

Hook Inventory

Pre-commit includes:

  • formatting and whitespace normalization
  • pyupgrade autofix using the configured style.python_version
  • syntax validation for YAML, TOML, and JSON
  • merge-conflict, shebang, private-key, and large-file checks
  • shell linting and shell best-practice enforcement
  • direct-import, utility-centralization, SQL-centralization, and type-policy checks
  • repo-root Python version consistency checks for .python-version, pyproject.toml, mypy.ini, pyrightconfig.json, and ruff.toml
  • security, logging, dead-code, complexity, maintainability, and docstring checks
  • optional manifest and plan workflow validation
  • optional Gemini-powered ETHOS review
  • optional Go vet/test/lint stages

Pre-push re-runs the higher-signal checks over the pushed diff, including full Gemini review when enabled.

Most hook runtime and policy enforcement now lives in go/cmd/coding-ethos-hook-runner/. Python quality checks call Radon and Vulture directly from the Go runner, and shell, YAML, Docker, workflow, Go, and AI analyzer output is normalized there as well.

Commit-message hooks enforce:

  • conventional commit structure with a required scope
  • no AI attribution or promotional co-author lines

Updating

To update Go helper behavior:

make autofix
make go-tidy
make go-test
make go-lint

make go-fmt formats every Go source file under go/cmd/coding-ethos-hook-runner/. make go-tools-fmt runs the managed golangci-lint fmt formatter set for the shared Go module: gci, gofmt, gofumpt, goimports, and golines. make go-lint-fix runs managed golangci-lint run --fix; remaining diagnostics are structural lint findings that must be fixed in code. Override GO=/path/to/go or GOFMT=/path/to/gofmt when testing with a non-default toolchain.

Hook-runner tests install a test-only go guard that fails if a test reaches the host Go binary accidentally. Tests that intentionally exercise Go hook commands must install an explicit fake go binary in their fixture PATH. Run broad hook-runner tests with GO_TEST_TIMEOUT and, on Linux, an external process budget such as systemd-run --user --scope -p TasksMax=128.

Adding Hooks

Use Go for generic file, shell, text, source-aware, and commit-message checks. New hook behavior should land in focused files under go/cmd/coding-ethos-hook-runner/, with shared policy facts in go/internal/celexpr/, parser facts in go/internal/astfacts/ or go/internal/shellparse/, and tunable policy in coding_ethos.yml or config.yaml as appropriate. Do not add new policy-specific text scanners before checking the AST/CEL/SARIF path documented in docs/AST_CEL_SARIF_ARCHITECTURE.md.

Use Python only when the check genuinely depends on Python package behavior that cannot be represented by the shared Go parser/fact/CEL pipeline. The default direction for hook runtime and generated enforcement behavior is Go.

For hooks that modify files:

  • set stage_fixed: true
  • keep pre-commit.fail_on_changes: never
  • avoid stash-based workflows
  • keep output quiet unless the hook fails