runward

RW™ · V0.22.0

Docs · Decisions · ADR-0008

ADR-0008.

An opt-in hook seam around check

Date: 2026-07-07 Status: accepted Deciders: Thibault Souris (maintainer) Method: decision-loop — reality-check against the competitor benchmark (Spec Kit .specify/extensions.yml), challenge, durable position

Context

The competitor benchmark surfaced a clean extensibility pattern (P8): Spec Kit exposes before_/after_ hooks so third-party gates plug in without forking the core. runward's deterministic conformance gate is fixed and neutral, but an operator legitimately needs to run their own checks around it — a test-baseline validation (the residual of ADR-0005/P4), a security scan, a house linter. Forcing them to fork runward or wrap it in ad-hoc CI glue is friction.

The catch is security. runward's doctrine is least-privilege and untrusted-input-aware. Executing arbitrary shell from a file in a repository is a real surface: a cloned or malicious repo carrying a hooks file could run commands the moment someone types runward check. Any seam must not create surprise command execution.

Decision

An opt-in hook seam. runward check --hooks reads runward/hooks.json ({ "before": [...], "after": [...] }, arrays of shell commands) and runs the operator's commands around the audit: before first, then the deterministic audit, then after. A non-zero hook fails the gate — the operator wired it, so it gates for them.

  • Never by default. Plain runward check never runs hooks. Hooks execute only under the explicit --hooks flag, so cloning a repo and running check cannot execute anything.
  • The operator's extension, not runward's. runward's own conformance check stays deterministic and zero-LLM (ADR-0001); the hooks are the operator's commands, run with their explicit consent — the ecosystem model of npm scripts, git hooks, and Makefiles.
  • Where the P4 residual lives. An operator who wants baseline test validation, or any language-specific proof runward will not run itself, plugs their own runner here. runward still runs nothing of its own.

Alternatives discarded

  • Run hooks whenever hooks.json is present. A surprise-command-execution foot-gun on any clone; rejected on security grounds.
  • No seam at all. Forces forking or external wrapping for a need that is common and legitimate.
  • A hook DSL or an LLM-driven hook. Overkill; a list of shell commands is the neutral, universal interface, and keeps runward out of the business of interpreting them.

Consequences

  • Positive. The gate is extensible without forking; the P4 residual has a home; runward stays neutral and zero-run for its own part.
  • Negative, accepted. --hooks runs shell commands — documented as such, with the same trust posture as npm scripts or git hooks. The operator enables it only in repos they trust.
  • On other boundaries. A new runward/hooks.json convention and a --hooks flag on check; the deterministic audit and its zero-LLM invariant are untouched; hooks influence the exit code only when explicitly enabled.

Reevaluation trigger (mandatory, dated)

Reopen if the shell-command surface proves risky in practice — then restrict hooks to an allowlist, a signed-hooks scheme, or a restricted command vocabulary, rather than free shell.

Trigger set on: 2026-07-07 · Watched via: security review of the hook seam and any reported misuse.

References

  • ADR-0001 — the deterministic gate the seam wraps, never alters.
  • ADR-0005 — the P4 residual this seam hosts.
  • Spec Kit .specify/extensions.yml — the before/after hook reference.
  • src/lib/hooks.ts, src/commands/check.ts — the surfaces this ADR touches.
← All ADRs