Skip to content

grip_hook.config

grip_hook.config

Configuration loading.

Precedence, highest first:

  1. Command-line flags.
  2. GRIP_* environment variables.
  3. .grip.toml at the repository root.
  4. [tool.grip] in pyproject.toml at the repository root.
  5. Built-in defaults.

Config dataclass

Resolved grip configuration.

Source code in src/grip_hook/config.py
@dataclass(frozen=True, slots=True)
class Config:
    """Resolved grip configuration."""

    passing_score: int = DEFAULT_PASSING_SCORE
    """Minimum Grip Score (0-100) required to let the commit or push through."""

    provider: str = "anthropic"
    """Provider name: ``anthropic``, ``openai``, ``ollama``, ``claude-code``, ``codex``,
    ``gemini`` or ``fake``."""

    model: str = ""
    """Model identifier passed to the provider. Empty means the provider's default."""

    effort: str = "medium"
    """Reasoning effort for providers that support it. ``none`` disables the parameter."""

    api_key_env: str = ""
    """Environment variable holding the API key. Empty means the provider's default."""

    base_url: str = ""
    """Override the provider's API base URL (Ollama, proxies, OpenAI-compatible servers)."""

    difficulty: Difficulty = Difficulty.NORMAL
    """How demanding the questions and grading are."""

    max_diff_bytes: int = 200_000
    """Diffs larger than this are truncated before being sent to the provider."""

    exclude: tuple[str, ...] = DEFAULT_EXCLUDES
    """Path globs excluded from the diff (lockfiles, generated code)."""

    remember_passes_hours: float = 24.0
    """How long a passed diff stays valid without re-quizzing. ``0`` disables."""

    require_tty: bool = False
    """Fail instead of skipping when no interactive terminal is available."""

    fail_open: bool = False
    """Let the commit or push through when the provider errors out instead of blocking."""

    timeout: float = 120.0
    """Per-request timeout in seconds for provider calls."""

    def validate(self) -> Config:
        """Raise :class:`ConfigError` for out-of-range values and return ``self``."""
        if not 0 <= self.passing_score <= 100:
            raise ConfigError(f"passing_score must be between 0 and 100, got {self.passing_score}")
        if self.max_diff_bytes <= 0:
            raise ConfigError("max_diff_bytes must be positive")
        if self.remember_passes_hours < 0:
            raise ConfigError("remember_passes_hours must be zero or positive")
        if self.timeout <= 0:
            raise ConfigError("timeout must be positive")
        if not self.provider:
            raise ConfigError("provider must not be empty")
        return self

passing_score class-attribute instance-attribute

passing_score: int = DEFAULT_PASSING_SCORE

Minimum Grip Score (0-100) required to let the commit or push through.

provider class-attribute instance-attribute

provider: str = 'anthropic'

Provider name: anthropic, openai, ollama, claude-code, codex, gemini or fake.

model class-attribute instance-attribute

model: str = ''

Model identifier passed to the provider. Empty means the provider's default.

effort class-attribute instance-attribute

effort: str = 'medium'

Reasoning effort for providers that support it. none disables the parameter.

api_key_env class-attribute instance-attribute

api_key_env: str = ''

Environment variable holding the API key. Empty means the provider's default.

base_url class-attribute instance-attribute

base_url: str = ''

Override the provider's API base URL (Ollama, proxies, OpenAI-compatible servers).

difficulty class-attribute instance-attribute

difficulty: Difficulty = Difficulty.NORMAL

How demanding the questions and grading are.

max_diff_bytes class-attribute instance-attribute

max_diff_bytes: int = 200000

Diffs larger than this are truncated before being sent to the provider.

exclude class-attribute instance-attribute

exclude: tuple[str, ...] = DEFAULT_EXCLUDES

Path globs excluded from the diff (lockfiles, generated code).

remember_passes_hours class-attribute instance-attribute

remember_passes_hours: float = 24.0

How long a passed diff stays valid without re-quizzing. 0 disables.

require_tty class-attribute instance-attribute

require_tty: bool = False

Fail instead of skipping when no interactive terminal is available.

fail_open class-attribute instance-attribute

fail_open: bool = False

Let the commit or push through when the provider errors out instead of blocking.

timeout class-attribute instance-attribute

timeout: float = 120.0

Per-request timeout in seconds for provider calls.

validate

validate() -> Config

Raise :class:ConfigError for out-of-range values and return self.

Source code in src/grip_hook/config.py
def validate(self) -> Config:
    """Raise :class:`ConfigError` for out-of-range values and return ``self``."""
    if not 0 <= self.passing_score <= 100:
        raise ConfigError(f"passing_score must be between 0 and 100, got {self.passing_score}")
    if self.max_diff_bytes <= 0:
        raise ConfigError("max_diff_bytes must be positive")
    if self.remember_passes_hours < 0:
        raise ConfigError("remember_passes_hours must be zero or positive")
    if self.timeout <= 0:
        raise ConfigError("timeout must be positive")
    if not self.provider:
        raise ConfigError("provider must not be empty")
    return self

load_config

load_config(root: Path | None, *, env: dict[str, str] | None = None, overrides: dict[str, Any] | None = None) -> Config

Build the effective :class:Config for a repository.

Parameters:

Name Type Description Default
root Path | None

Repository root (where .grip.toml / pyproject.toml live), or None.

required
env dict[str, str] | None

Environment mapping. Defaults to :data:os.environ.

None
overrides dict[str, Any] | None

Values from command-line flags. None entries are ignored.

None
Source code in src/grip_hook/config.py
def load_config(
    root: Path | None,
    *,
    env: dict[str, str] | None = None,
    overrides: dict[str, Any] | None = None,
) -> Config:
    """Build the effective :class:`Config` for a repository.

    Args:
        root: Repository root (where ``.grip.toml`` / ``pyproject.toml`` live), or ``None``.
        env: Environment mapping. Defaults to :data:`os.environ`.
        overrides: Values from command-line flags. ``None`` entries are ignored.
    """
    cfg = Config()
    if root is not None:
        pyproject = root / "pyproject.toml"
        if pyproject.is_file():
            tool = _read_toml(pyproject).get("tool", {})
            if isinstance(tool, dict) and isinstance(tool.get("grip"), dict):
                cfg = _apply(cfg, tool["grip"], str(pyproject))
        grip_toml = root / ".grip.toml"
        if grip_toml.is_file():
            cfg = _apply(cfg, _read_toml(grip_toml), str(grip_toml))

    environ = os.environ if env is None else env
    env_raw = {
        key[len(ENV_PREFIX) :].lower(): value
        for key, value in environ.items()
        if key.startswith(ENV_PREFIX) and key[len(ENV_PREFIX) :].lower() in _FIELD_NAMES
    }
    cfg = _apply(cfg, env_raw, "environment")

    if overrides:
        cfg = _apply(cfg, {k: v for k, v in overrides.items() if v is not None}, "command line")
    return cfg.validate()

describe

describe(cfg: Config) -> list[tuple[str, str]]

Return (name, value) pairs for display.

Source code in src/grip_hook/config.py
def describe(cfg: Config) -> list[tuple[str, str]]:
    """Return ``(name, value)`` pairs for display."""
    rows: list[tuple[str, str]] = []
    for f in fields(cfg):
        value = getattr(cfg, f.name)
        if isinstance(value, tuple):
            text = ", ".join(value) if value else "(none)"
        elif isinstance(value, Difficulty):
            text = value.value
        else:
            text = str(value) if value != "" else "(provider default)"
        rows.append((f.name, text))
    return rows