feat: add post-receive hooks sandboxed via kefka
Implement automatic hook execution after a successful push: when started with
-allow-hooks, objgitd runs .objgit/hooks/receive-pack from the pushed commit
in a restricted shell (kefka virtual bash). The hook sees a read-only view of
the commit tree at /src and writable scratch at /tmp. Execution is async
post-response so hooks cannot reject a push. Only coreutils commands available
(cat, grep, ls, head, tail, sort, sha256sum, etc.) — no arbitrary binaries,
network, or git command.
Refs before/after push are snapshotted to detect branch changes since
transport.ReceivePack does not report them. One hook runs per created/updated
branch; deletions are skipped. Output and exit status logged to slog only.
New packages:
- internal/treefs: lazy read-only billy.Filesystem view of a git tree
(blobs fetched on open, no checkout to disk)
- internal/mountfs: path-prefix composite FS routing /src and /tmp to
separate mounted filesystems
- internal/kefkash: vendored copy of kefka's billysh handler wiring
(adapted to allow writes so /tmp redirections work)
Includes: daemon integration, flags (-allow-hooks, -hook-timeout), tests
(treefs unit tests, diffRefs, e2e push tests), example hook with regression
test, and CLAUDE.md architecture documentation.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
feat(hooks): stream push-hook output to client live over sideband
Hooks now run synchronously and stream stdout/stderr to the pushing client
as "remote: ..." lines over the git sideband progress channel (band 2).
Previously they ran asynchronously after the push response, and output was
captured to slog only — the client never saw it.
To support streaming, fork go-git's transport.ReceivePack (v6) into
cmd/objgitd/receivepack.go as receivePackStreaming(). go-git keeps the
sideband Muxer internal and sends the closing flush-pkt before returning,
so there's no public seam to inject progress. The fork adds one callback
onUpdated(progress io.Writer), invoked after report-status but before the
final flush. When sideband is negotiated, progress writes to band 2
(ProgressMessage); otherwise it's nil and hooks fall back to logging.
HTTP adds a flush-on-write wrapper (flushWriter) to the receive-pack
response so net/http buffering doesn't hold the "remote:" lines back.
git:// and SSH write through live sockets and need no such wrapper.
Remove the now-dead async machinery: hookWG field and its shutdown drain.
Hooks complete before receivePackStreaming returns, so the connection
already holds them until completion.
Update tests to assert hook output appears in the push output (as "remote:"
lines) rather than in logs. Add TestSmartHTTPHookStreams to cover the HTTP
flush path. All three transports (git://, HTTP, SSH) verified.
Note: Clients now wait for hooks to finish (bounded by -hook-timeout,
default 60s) before the push completes, rather than push completing while
hooks run in the background. This is inherent to streaming output live.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>