Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
59a7082
feat(firewall): opt-in ~ regex marker for egress path rules
schmitthub Jun 23, 2026
de09b51
fix(firewall): honor ~ regex path marker in host-proxy egress gate
schmitthub Jun 23, 2026
cf0f2ce
fix(hostproxy): canonicalize dot-segment dirs to match Envoy normaliz…
schmitthub Jun 23, 2026
315825d
docs(firewall): sharpen domain wildcard and regex path rule guidance
schmitthub Jun 23, 2026
c3f2190
feat(firewall): validate literal path chars, document rule precedence
schmitthub Jun 23, 2026
f42acb1
fix(iostreams): wrap changelog teaser, strip changelog URLs, fix flak…
schmitthub Jun 23, 2026
228b962
fix(hostproxy): fail egress enforcement closed like Envoy
schmitthub Jun 23, 2026
9a83e0a
refactor(controlplane): relocate test mocks to mocks/ subpackages, ha…
schmitthub Jun 23, 2026
cb268bd
chore(golanglint): added draconian linter rules
schmitthub Jun 23, 2026
fb5e07d
chore(golanglint): fixed schema
schmitthub Jun 23, 2026
7fe530c
chore(golanglint): fixed schema
schmitthub Jun 23, 2026
bc95872
chore(golang-lint): removed autofixer
schmitthub Jun 23, 2026
b8a53b2
chore(docs): stale docs
schmitthub Jun 23, 2026
3c29341
chore(golang-lint): added stable config
schmitthub Jun 23, 2026
a2cded2
chore(hooks): added git checks
schmitthub Jun 23, 2026
8ae800a
chore(hooks): hardened checks to prevent pre-commit skip var bypass
schmitthub Jun 23, 2026
d260d46
chore(lint): removed depguard from checks
schmitthub Jun 23, 2026
7ba7d77
chore(lint): added wsl_v5 to disable list
schmitthub Jun 24, 2026
acaedc7
chore(lint): more forgiving rules
schmitthub Jun 24, 2026
fb5dbe3
fix(review): pre-linter fixes
schmitthub Jun 24, 2026
d1020b1
chore(lint): resolve all golangci-lint-full findings on the branch
schmitthub Jun 24, 2026
eee4d6c
chore(linter): relax containedctx et al for stubs and mocks
schmitthub Jun 24, 2026
e37a305
chore(linter): contextcheck on mocks
schmitthub Jun 24, 2026
5a8ae88
chore(linter): revert
schmitthub Jun 24, 2026
088ce13
refactor(mocks): store ctx directly in FakeSessionStream
schmitthub Jun 24, 2026
ea892bb
chore(clawker): regex test
schmitthub Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/agents/test-hunter.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ cat internal/docker/mocks/*.go # FakeClient, fixtures
cat internal/git/gittest/*.go # InMemoryGitManager
cat internal/config/mocks/*.go # ConfigMock, NewBlankConfig, NewFromString, NewIsolatedTestConfig
cat internal/project/mocks/*.go # ProjectManagerMock, TestManagerHarness
cat internal/controlplane/mocks/*.go # ControlPlaneServiceMock, ManagerMock, IntrospectorMock, AdminServiceClientMock
cat api/admin/v1/mocks/*.go controlplane/manager/mocks/*.go controlplane/auth/mocks/*.go # AdminServiceClientMock, ManagerMock, IntrospectorMock
cat internal/controlplane/firewall/ebpf/mocks/*.go # EBPFManagerMock
cat internal/hostproxy/hostproxytest/*.go # MockHostProxy
cat pkg/whail/whailtest/*.go # FakeAPIClient, BuildKitCapture, golden file seeding
Expand Down
5 changes: 3 additions & 2 deletions .claude/docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ Key packages:
- `controlplane/firewall/ebpf/netlogger` — userspace consumer of the BPF `events_ringbuf`. Enriches per-decision records with `{container_id, agent, project, domain}` via pub/sub enrollment events + dockerevents eviction, ships OTLP log records (`service.name=ebpf-egress`) through `otel.NewOtelLoggerProvider` to the trusted-infra OTLP receiver. See `controlplane/firewall/ebpf/netlogger/CLAUDE.md`.
- `controlplane/firewall/ebpf/cmd` — break-glass `ebpf-manager` CLI bundled alongside `clawkercp` in the container image.
- `controlplane/manager` — Host-side CP lifecycle: `EnsureRunning`/`Stop`/`CPRunning`, `BuildCPContainerConfig`, `Manager` interface + `NewManager`, embedded clawkercp + ebpf-manager binaries (`go:embed`). Split out so `cmd/clawkercp` can import the CP packages without dragging in embed directives for its own binary.
- `controlplane/mocks` — moq-generated: `IntrospectorMock`, `AdminServiceClientMock`.
- `api/admin/v1/mocks` — moq-generated `AdminServiceClientMock`; `controlplane/auth/mocks` — moq-generated `IntrospectorMock`.
- `clawkerd/embed` — `//go:embed assets/clawkerd` (package `clawkerdembed`) exports the per-container daemon binary as `clawkerdembed.Binary`; bundler drops it into every per-project image at `/usr/local/bin/clawkerd`.
- `clawkerd` — per-container agent daemon (package): mTLS listener on `:7700`, `ClawkerdService.Session` bidi-stream for CP command dispatch, `registerCoordinator` for one-time CP-triggered Register handshake. Boot sequence in `clawkerd/CLAUDE.md`. `cmd/clawkerd` is the thin entrypoint (`os.Exit(clawkerd.Main())`); `Main`/`run` live in `internal/clawkerd`.
- `api/admin/v1` — AdminService proto + method-scope registration (`AdminMethodScopes`, covered by `TestAdminMethodScopes_CoversAllRPCs`).
Expand Down Expand Up @@ -770,7 +770,8 @@ Each package with complex dependencies provides test infrastructure:
| `project/mocks/` | `NewMockProjectManager()`, `NewMockProject(name, repoPath)`, `NewTestProjectManager(t, gitFactory)` |
| `git/gittest/` | `InMemoryGitManager` (memfs-backed, seeded with initial commit) |
| `whail/whailtest/` | `FakeAPIClient` (80+ Fn fields, call recording), build scenarios, `EventRecorder` |
| `controlplane/mocks/` | `IntrospectorMock`, `AdminServiceClientMock` (moq-generated) |
| `api/admin/v1/mocks/` | `AdminServiceClientMock` (moq-generated) |
| `controlplane/auth/mocks/` | `IntrospectorMock` (moq-generated) |
| `controlplane/manager/mocks/` | `ManagerMock` (moq-generated) for host-side CP lifecycle noun |
| `controlplane/agent/` (test-only) | `RegistryMock` (moq-generated, lives in package itself to avoid import cycle) |
| `controlplane/firewall/ebpf/mocks/` | `EBPFManagerMock` (moq-generated) |
Expand Down
8 changes: 4 additions & 4 deletions .claude/docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ CP crashing is not an availability problem — it is a security boundary integri

1. **No `panic()`. No `log.Fatal()`. No `os.Exit()`.** Constructors return `(nil, error)`. main.go logs and degrades.
2. **Long-lived goroutines must `recover()`.** Heartbeats, watchers, RPC handlers — one bad event must not silently strand eBPF.
3. **Subsystem failures degrade, never escalate.** Broken Executor → `initExec = nil`; broken dialer → `dialer = nil`. Everything else stays up. The patterns in `cmd/clawkercp/main.go` — `wireInitExecutor` (initExec; emits `event=agent_init_executor_unavailable`) and the `agent.New(...)` block that degrades on error to `event=agent_dialer_unavailable` — are canonical.
3. **Subsystem failures degrade, never escalate.** Broken Executor → `executor = nil`; broken dialer → `dialer = nil`. Everything else stays up. The patterns in `cmd/clawkercp/main.go` — `wireExecutor` (executor; emits `event=agent_executor_unavailable`) and the `agent.New(...)` block that degrades on error to `event=agent_dialer_unavailable` — are canonical.
4. **Every degraded path emits a structured log line** (`event=<subsystem>_unavailable`) with component, error, and blast-radius fields. Operators will not see panic stacks; the structured log is the only surface.
5. **The only acceptable hard-exits** are pre-`SetReady` startup gates (no agents running yet, eBPF not load-bearing) and the orchestrator's intentional drain-to-zero clean exit.

Expand Down Expand Up @@ -474,7 +474,7 @@ with all closures wired *cmdutil.Factory
Factory is a pure struct with closure/value fields — no methods. 3 eager (set directly), rest lazy (closures):

**Eager**: `Version` (string), `IOStreams` (`*iostreams.IOStreams`), `TUI` (`*tui.TUI`)
**Lazy**: `Config` (`func() (config.Config, error)`), `Client` (`func(ctx) (*docker.Client, error)`), `Logger` (`func() (*logger.Logger, error)`), `ProjectManager` (`func() (project.ProjectManager, error)`), `GitManager` (`func() (*git.GitManager, error)`), `HostProxy` (`func() hostproxy.HostProxyService`), `SocketBridge` (`func() socketbridge.SocketBridgeManager`), `Prompter` (`func() *prompter.Prompter`), `AdminClient` (`func(ctx) (adminv1.AdminServiceClient, error)`), `ControlPlane` (`func() manager.Manager`), `HttpClient` (`func() *http.Client`)
**Lazy**: `Config` (`func() (config.Config, error)`), `Client` (`func(ctx) (*docker.Client, error)`), `Logger` (`func() (*logger.Logger, error)`), `ProjectManager` (`func() (project.ProjectManager, error)`), `GitManager` (`func() (*git.GitManager, error)`), `HostProxy` (`func() hostproxy.Service`), `SocketBridge` (`func() socketbridge.SocketBridgeManager`), `Prompter` (`func() *prompter.Prompter`), `AdminClient` (`func(ctx) (adminv1.AdminServiceClient, error)`), `ControlPlane` (`func() manager.Manager`), `HttpClient` (`func() *http.Client`)

The constructor in `internal/cmd/factory/default.go` wires all closures. Commands extract closures into per-command Options structs. Run functions only accept `*Options`, never `*Factory`.

Expand Down Expand Up @@ -899,10 +899,10 @@ and plugin installation on every container creation.

**Init flow** (orchestrated by `shared.CreateContainer()` in `cmd/container/shared/container.go`):

Progress streamed via events channel (`chan CreateContainerEvent`). Steps:
Developer diagnostics go to zerolog; the caller owns all terminal output. Steps:
1. **workspace** — `workspace.SetupMounts()` (internally calls `EnsureConfigVolumes()`)
2. **config** (skipped if volume cached) — `containerfs.PrepareClaudeConfig()` + `containerfs.PrepareCredentials()` → `docker.CopyToVolume()`
3. **environment** — `shared.ResolveAgentEnv()` merges env_file/from_env/env → runtime env vars (warnings sent as `MessageWarning` events)
3. **environment** — `shared.ResolveAgentEnv()` merges env_file/from_env/env → runtime env vars (warnings surfaced to the caller on the result)
4. **container** — validate flags, `BuildConfigs()`, `docker.ContainerCreate()` + `InjectPostInitScript()` (when `agent.post_init` configured). Onboarding bypass is image-level: entrypoint seeds `~/.claude/.config.json` from staged defaults

**Key packages**: `internal/containerfs` (tar preparation, path rewriting),
Expand Down
11 changes: 5 additions & 6 deletions .claude/docs/KEY-CONCEPTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Type and abstraction index for the clawker codebase. Use this when you need a one-line reminder of what a named type does and which package owns it. For the full API of any symbol, read the package-specific `internal/<pkg>/CLAUDE.md` — they're lazy-loaded on demand.

> **CP crashing is a SECURITY incident.** eBPF programs are CP-managed but kernel-pinned under `/sys/fs/bpf` and survive CP container death. Clean lifecycle (`Stack.Stop` → `ebpfMgr.FlushAll`) only runs on the orchestrator's drain-to-zero path; panics, `log.Fatal`, and unrecovered goroutines all skip it. After a CP crash, agent containers keep running, eBPF keeps filtering against frozen rules, but CP is no longer there to update rules, expire bypasses, observe behavior, or dispatch containment. The user's mental model "CP has my agents covered" is silently false. **Therefore: no panics in code reachable from `cmd/clawkercp/main.go` after `SetReady`. Constructors return `(nil, error)`; long-lived goroutines must `recover()`; subsystem failures degrade (`initExec = nil`, `dialer = nil`) instead of crashing the daemon.** See root `CLAUDE.md` and `controlplane/CLAUDE.md` for the full statement and templates.
> **CP crashing is a SECURITY incident.** eBPF programs are CP-managed but kernel-pinned under `/sys/fs/bpf` and survive CP container death. Clean lifecycle (`Stack.Stop` → `ebpfMgr.FlushAll`) only runs on the orchestrator's drain-to-zero path; panics, `log.Fatal`, and unrecovered goroutines all skip it. After a CP crash, agent containers keep running, eBPF keeps filtering against frozen rules, but CP is no longer there to update rules, expire bypasses, observe behavior, or dispatch containment. The user's mental model "CP has my agents covered" is silently false. **Therefore: no panics in code reachable from `cmd/clawkercp/main.go` after `SetReady`. Constructors return `(nil, error)`; long-lived goroutines must `recover()`; subsystem failures degrade (`executor = nil`, `dialer = nil`) instead of crashing the daemon.** See root `CLAUDE.md` and `controlplane/CLAUDE.md` for the full statement and templates.

> **CP ≠ firewall.** Common LLM confusion. CP is unconditional infrastructure (auth, gRPC AdminService on AdminPort, AgentService listener on AgentPort, agent registry, CP→clawkerd `agent.Dialer` outbound dialer, pub/sub event topics, mTLS, owns clawker-net). The firewall is one optional subsystem CP manages (Envoy + CoreDNS + eBPF egress enforcement), toggled by `firewall.enable` in `settings.yaml` (the master switch is global, NOT in `clawker.yaml` — the project schema's `security.firewall` holds per-project `add_domains`/`rules` only). Disabling firewall does NOT disable CP, agent registry, agent.Dialer→clawkerd Session, ListAgents, or any non-firewall AdminService RPC. Don't gate non-firewall behavior on the firewall flag.

Expand All @@ -15,10 +15,9 @@
| `WorkspaceStrategy` | Bind (live mount) vs Snapshot (ephemeral copy) |
| `PTYHandler` | Raw terminal mode, bidirectional streaming (in `docker` package) |
| `ContainerConfig` | Labels, naming (`clawker.project.agent`), volumes |
| `CreateContainer()` | Single entry point for container creation (workspace, config, env, create, inject); shared by `run` and `create` via events channel for progress |
| `CreateContainer()` | Single entry point for container creation (workspace, config, env, create, inject); shared by `run` and `create`. Diagnostics to zerolog; caller owns terminal output |
| `IsOutsideHome(dir)` | Pure bool function in `container/shared/safety.go` — returns true when dir is `$HOME` or not within `$HOME`. Used by `run`/`create` (prompt) |
| `CreateContainerConfig` / `CreateContainerResult` | Input/output types for `CreateContainer()` — all deps and runtime values |
| `CreateContainerEvent` | Channel event: Step, Status (`StepRunning`/`StepComplete`/`StepCached`), Type (`MessageInfo`/`MessageWarning`), Message |
| `clawker-share` | Optional read-only bind mount from `cfg.ShareSubdir()` into containers at `~/.clawker-share` when `agent.enable_shared_dir: true`; host dir created during `clawker project init`, re-created if missing during mount setup |
| `containerfs` | Host Claude config preparation for container init: copies settings, plugins (incl. cache), credentials to config volume; rewrites host paths in plugin JSON files; prepares post-init script tar |
| `ConfigVolumeResult` | Bool flags tracking which config volumes were freshly created (`ConfigCreated`, `HistoryCreated`) — returned by `workspace.EnsureConfigVolumes` |
Expand All @@ -45,7 +44,7 @@
| `manager.Manager` | Factory-facing noun (`f.ControlPlane()`) wrapping host-side CP container lifecycle: `EnsureRunning`, `Stop`, `IsRunning`, `ProbeHealthz`. Lives in `controlplane/manager/`. Consumed by the break-glass `clawker controlplane up/down/status` verbs |
| `manager.EnsureRunning` | Package-level host-side CP container bootstrap (idempotent, mutex-guarded, mount-mode reconciliation, health-poll). Consumed via `ensureRunning` seam by `adminClientFunc` |
| `controlplane.AgentWatcher` | Polls Docker for `purpose=agent` containers; on drain-to-zero (past grace/threshold, `ListErrCeiling`-bounded) fires drain callback for CP self-shutdown (INV-B2-007). `Run` is at-most-once (`atomic.Bool`) |
| `f.AdminClient(ctx)` | Factory lazy noun returning `adminv1.AdminServiceClient` — transparent CP bootstrap on first call, mTLS + OAuth2 + keepalive. Rebuilds `grpc.ClientConn` only on `TransientFailure`/`Shutdown`. Mock: `controlplane/mocks.AdminServiceClientMock` |
| `f.AdminClient(ctx)` | Factory lazy noun returning `adminv1.AdminServiceClient` — transparent CP bootstrap on first call, mTLS + OAuth2 + keepalive. Rebuilds `grpc.ClientConn` only on `TransientFailure`/`Shutdown`. Mock: `api/admin/v1/mocks.AdminServiceClientMock` |
| `agent.Registry` | Sqlite-persisted record of registered agents keyed by SHA-256 over the mTLS peer cert DER (`[sha256.Size]byte`). CP is the SOLE writer — writes rows via Register handler, evicts via dockerevents `container/destroy` + startup reap. Rows store `(thumbprint, container_id, project, agent_name, registered_at, last_seen)` with `project` / `agent_name` typed as `auth.ProjectSlug` / `auth.AgentName`; the displayed `AgentFullName` is reconstructed on demand. Surface: `Add`, `LookupByContainerID` (used by Register handler for idempotency + dialer for thumbprint-replay detection), `EvictByContainerID`, `Snapshot`. Goose-managed schema (migrations co-located + embedded). Backing store: `modernc.org/sqlite`. Lives in `controlplane/agent` |
| `pubsub.Topic[T]` / `pubsub.Event[T]` | Generic in-memory pub/sub pipe for the CP — a dumb, stateless transport that knows only typed envelopes and subscribers, never agents/containers/firewalls. `Topic[T].Subscribe(func(Event[T]))` registers a typed handler; `Publish` is non-blocking with a back-pressure signal. Each subscriber gets a bounded buffer with drop-oldest on overflow (counted); every delivery runs under recover so one panicking subscriber can't kill PID 1 and strand eBPF (CP §3.4). No central worldview — each domain owns its own state. Lives in `controlplane/pubsub` |
| Bounded-context state repositories | Each CP domain (`agent`, `dockerevents`, …) owns and projects its OWN private `Store`/`Repository`; a subscriber folds typed events into its store. No central `State`/`Snapshot`. Cross-domain reads happen only through a read-only interface the owning domain chooses to expose, injected by the orchestrator (`internal/controlplane/cmd.go`) — never direct state access |
Expand All @@ -62,8 +61,8 @@
| `controlplane.AgentMethodScopes()` | Per-listener scope vocabulary for the agent gRPC listener. Currently one entry: `Register → ScopeSelfRegister`. Mirror of `AdminMethodScopes`; an `AuthInterceptor` wired with this map fails closed on unmapped methods, so a new RPC added without a scope entry is rejected at runtime |
| `shared.CommandOpts` | DI container for container start orchestration — function closures: Client, Config, ProjectManager, HostProxy, ControlPlane, AdminClient, SocketBridge, Logger; plus plain string fields AgentName and Project |
| `shared.ContainerStart()` | Three-phase container start: `BootstrapServicesPreStart` → docker start → `BootstrapServicesPostStart` (3 RPCs: FirewallInit → FirewallAddRules → FirewallEnable). Used by `run` and `start` |
| `hostproxy.HostProxyService` | Interface for host proxy operations (EnsureRunning, IsRunning, ProxyURL); mock: `hostproxytest.MockManager` |
| `hostproxy.Manager` | Concrete host proxy daemon manager (spawns subprocess); implements `HostProxyService` |
| `hostproxy.Service` | Interface for host proxy operations (EnsureRunning, IsRunning, ProxyURL); mock: `hostproxytest.MockManager` |
| `hostproxy.Manager` | Concrete host proxy daemon manager (spawns subprocess); implements `Service` |
| `socketbridge.SocketBridgeManager` | Interface for socket bridge operations; mock: `sockebridgemocks.SocketBridgeManagerMock` |
| `socketbridge.Manager` | Per-container SSH/GPG agent bridge daemon (muxrpc over docker exec) |
| `iostreams.IOStreams` | I/O streams, TTY detection, colors, styles, spinners, progress, layout |
Expand Down
3 changes: 2 additions & 1 deletion .claude/docs/TESTING-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ Each package with complex dependencies provides test infrastructure:
| `internal/git` | `gittest/` | `InMemoryGitManager` |
| `internal/project` | `mocks/` | `NewMockProjectManager()`, `NewMockProject(name, repoPath)`, `NewTestProjectManager(t, gitFactory)` |
| `pkg/whail` | `whailtest/` | `FakeAPIClient`, build scenarios, `EventRecorder` |
| `internal/controlplane` | `mocks/` | `AdminServiceClientMock`, `IntrospectorMock` (moq) |
| `api/admin/v1` | `mocks/` | `AdminServiceClientMock` (moq) |
| `controlplane/auth` | `mocks/` | `IntrospectorMock` (moq) |
| `internal/controlplane/cpboot` | `mocks/` | `ManagerMock` (moq) |
| `internal/controlplane/firewall/ebpf` | `mocks/` | `EBPFManagerMock` (moq) |
| `internal/hostproxy` | `hostproxytest/` | `MockHostProxy`, `MockManager` |
Expand Down
46 changes: 46 additions & 0 deletions .claude/hooks/git-checks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -uo pipefail

INPUT=$(cat)
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$CMD" ] && exit 0

printf '%s' "$CMD" | grep -qiE '\bgit\b' || exit 0

UNQUOTED=$(printf '%s' "$CMD" | sed -E "s/'[^']*'//g; s/\"[^\"]*\"//g")

makes_commit() {
printf '%s' "$UNQUOTED" | grep -qE '\bgit\b[^|;&]*\b(commit|merge)\b'
}

block() {
printf 'BLOCKED by git-checks hook: %s\n' "$1" >&2
printf 'This guard keeps every commit going through the repo pre-commit hooks. If this is a genuine emergency, ask the user to run it on the host.\n' >&2
exit 2
}

if printf '%s' "$UNQUOTED" | grep -qE -- '--no-verify'; then
block "--no-verify skips git hooks"
fi

if printf '%s' "$UNQUOTED" | grep -qE '\bgit\b[^|;&]*\b(commit|merge)\b[^|;&]*(^|[[:space:]])-[A-Za-z]*n[A-Za-z]*([[:space:]]|$)'; then
block "git commit/merge -n skips the pre-commit hook"
fi

if printf '%s' "$UNQUOTED" | grep -qiE '\bgit\b[^|;&]*-c[[:space:]=]+core\.hookspath' && makes_commit; then
block "inline -c core.hooksPath override skips the pre-commit hook for this commit"
fi

if printf '%s' "$CMD" | grep -qE '\b(GIT_CONFIG_(COUNT|KEY_[0-9]+|VALUE_[0-9]+))=' && makes_commit; then
block "inline GIT_CONFIG_* override can disable the pre-commit hook for this commit"
fi

if printf '%s' "$CMD" | grep -qE '(^|[[:space:]])SKIP=' && makes_commit; then
block "SKIP= drops one or more pre-commit hooks for this commit"
fi

if printf '%s' "$UNQUOTED" | grep -qE '\bgit\b[^|;&]*\b(commit-tree|fast-import)\b'; then
block "git plumbing (commit-tree/fast-import) bypasses pre-commit entirely"
fi

exit 0
Loading
Loading