Skip to content

feat(hooks): automatic session-commit linking via git hooks#100

Merged
rsnodgrass merged 4 commits intomainfrom
ryan/session-init-work
Mar 3, 2026
Merged

feat(hooks): automatic session-commit linking via git hooks#100
rsnodgrass merged 4 commits intomainfrom
ryan/session-init-work

Conversation

@rsnodgrass
Copy link
Copy Markdown
Contributor

@rsnodgrass rsnodgrass commented Mar 3, 2026

Summary

Implements automatic linking of ox session recordings to git commits via a prepare-commit-msg hook. The hook appends configurable trailers (Co-Authored-By, SageOx-Session) to commits, installed automatically as part of ox init, ox integrate install, and ox doctor repair workflows.

What Changed

  • New Commands: ox hooks commit-msg (deterministic hook called by git), ox hooks install/uninstall (helper functions)
  • New Doctor Check: git-commit-hooks slug detects and auto-repairs missing or stale hook installations
  • Core Infrastructure: buildSessionURL() shared utility, Attribution.Session field for per-field trailer control
  • Automatic Setup: Git hooks installed during ox init, ox integrate install, and ox doctor --fix

Test Plan

  • All 13 new tests pass (git hook install/uninstall/idempotent, commit message append, URL builder)
  • Full test suite: 5513 tests pass (1 pre-existing failure unrelated to this PR)
  • Manual verification: hook installed, trailers appended correctly, core.hooksPath respected, idempotent operations work

Session Recording

View session recording

Co-Authored-By: SageOx ox@sageox.ai

Summary by CodeRabbit

  • New Features

    • Session URLs are now recorded and surfaced for agent recordings.
    • Git commit hooks append session and Co-Authored-By trailers to commits when recording.
    • CLI now reports and manages Git commit hook status during integration/install flows.
  • Tests

    • Added tests for git hook install/uninstall/verification and session URL construction.
  • Chores

    • Added a diagnostic check to verify Git commit hooks and offer automatic fixes.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 3, 2026

Warning

Rate limit exceeded

@rsnodgrass has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 34 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 7d69056 and e9f8147.

📒 Files selected for processing (4)
  • cmd/ox/agent_prime.go
  • cmd/ox/hooks_commit_msg_test.go
  • cmd/ox/hooks_git.go
  • cmd/ox/session_url.go
📝 Walkthrough

Walkthrough

Adds deterministic Git prepare-commit-msg hooks, installation/uninstallation utilities, a doctor check to verify/install hooks, commit-message trailer appending logic (Co-Authored-By, SageOx-Session), session URL construction, session attribution support, and minor agent bootstrap session URL exposure.

Changes

Cohort / File(s) Summary
Git Hook Management
cmd/ox/hooks_git.go, cmd/ox/hooks_git_test.go
Add idempotent install/uninstall/has checks for prepare-commit-msg hooks, respect core.hooksPath, preserve existing hooks via marker-delimited sections, and extensive unit tests covering scenarios and permissions.
Commit Message Hook
cmd/ox/hooks_commit_msg.go, cmd/ox/hooks_commit_msg_test.go
Add hidden hooks commit-msg command used by prepare-commit-msg hook: skips merges, resolves project+user attribution, appends Co-Authored-By and SageOx-Session trailers deterministically using git interpret-trailers; includes unit tests and helpers (resolveProjectAttribution).
Hooks Command Registration
cmd/ox/hooks_cmd.go
Introduce hidden parent Cobra command hooks and register it on CLI for hook/CI invocation.
Session URL Construction & Use
cmd/ox/session_url.go, cmd/ox/session_view_html.go, cmd/ox/hooks_commit_msg.go, cmd/ox/hooks_commit_msg_test.go
Add centralized buildSessionURL(cfg, sessionName) helper; refactor session view and hook code to use it and fail early when URL cannot be built; tests validate URL variants and normalization.
Attribution: Session Support
internal/config/attribution.go, internal/config/attribution_test.go
Add session attribution field to Attribution and ResolvedAttribution, with GetSession and IsSessionSet helpers and merge behavior; tests for defaults, overrides, and helpers.
Doctor / Verification
cmd/ox/doctor.go, cmd/ox/doctor_git_commit_hooks.go, cmd/ox/doctor_types.go, cmd/ox/doctor_types_test.go
Introduce git-commit-hooks doctor check (slug CheckSlugGitCommitHooks) that verifies prepare-commit-msg hooks, supports --fix to install hooks, and registers the check in the Integration category.
Integration & Init Flow
cmd/ox/integrate.go, cmd/ox/init.go
Wire git commit hook install/uninstall/status into integrate and init flows: attempt install during integration/init, uninstall on integration removal, show hook status in listings, and include hooks in uninstall-all detection.
Agent bootstrap
cmd/ox/agent_prime.go
Add SessionURL string to sessionStatus and populate it when bootstrapping agent prime while a recording is active (derives session name and uses buildSessionURL).

Sequence Diagram

sequenceDiagram
    participant Git as Git
    participant Hook as prepare-commit-msg Hook
    participant CLI as ox hooks command
    participant Config as Project Config & Attribution
    participant Trailers as git interpret-trailers

    Git->>Hook: trigger prepare-commit-msg (file, source)
    Hook->>CLI: invoke hidden `ox hooks commit-msg` with args
    CLI->>Config: load project config & resolve attribution
    Config-->>CLI: attribution + session state
    CLI->>CLI: determine trailers (Co-Authored-By, SageOx-Session)
    CLI->>Trailers: run `git interpret-trailers --in-place` to append trailers
    Trailers-->>CLI: success/failure (logged)
    CLI-->>Git: exit (do not block commit on failures)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

sageox

Poem

🐰 I hopped into hooks with a twitch of my nose,

trailers appended where tidy prose grows,
sessions now link from commits in a blink,
installs stay gentle — no chaos, I think,
hooray for hooks — hop on, let's go! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: automatic linking of session recordings to git commits via hooks. It directly captures the primary feature being implemented across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ryan/session-init-work

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
cmd/ox/integrate.go (1)

219-225: Differentiate “already installed” vs “installed now” for clearer UX.

Because InstallGitHooks is idempotent, repeated runs currently report a fresh install even when no change occurred. Consider checking HasGitHooks first to keep status output accurate.

Proposed refinement
-	// install git commit hooks (prepare-commit-msg for trailers)
-	if err := InstallGitHooks(gitRoot); err != nil {
+	// install git commit hooks (prepare-commit-msg for trailers)
+	if HasGitHooks(gitRoot) {
+		fmt.Println(ui.PassStyle.Render("✓") + " Git commit hooks already installed")
+		fmt.Println("  - prepare-commit-msg (Co-Authored-By, SageOx-Session trailers)")
+	} else if err := InstallGitHooks(gitRoot); err != nil {
 		cli.PrintWarning(fmt.Sprintf("Could not install git commit hooks: %v", err))
 	} else {
 		fmt.Println(ui.PassStyle.Render("✓") + " Git commit hooks installed")
 		fmt.Println("  - prepare-commit-msg (Co-Authored-By, SageOx-Session trailers)")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/ox/integrate.go` around lines 219 - 225, The UX currently always prints
"Git commit hooks installed" even when hooks were already present because
InstallGitHooks is idempotent; call HasGitHooks(gitRoot) first to detect
existing hooks and print a distinct message like "Git commit hooks already
installed" when true, and only call InstallGitHooks when HasGitHooks returns
false, handling and logging errors from InstallGitHooks as before and printing
the success message only after a real install; reference the HasGitHooks and
InstallGitHooks functions and the existing status printing logic for where to
make this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/ox/hooks_commit_msg_test.go`:
- Around line 13-40: Both tests mutate package globals hooksCommitMsgSource and
hooksCommitMsgFile without restoring them and ignore some I/O errors; fix by
saving original values of hooksCommitMsgSource and hooksCommitMsgFile at the
start of each test and restoring them in a defer, and replace ignored error
returns with require.NoError checks (e.g. check os.Getwd() error into origDirErr
with require.NoError, and check os.ReadFile/os.Chdir errors with
require.NoError) so runHooksCommitMsg tests clean up state and fail on I/O
errors; update TestRunHooksCommitMsg_SkipsMergeSource and
TestRunHooksCommitMsg_NoopWhenNotInitialized accordingly while still calling
runHooksCommitMsg.

In `@cmd/ox/hooks_git.go`:
- Around line 64-68: The current hook-install check only looks for
oxHookMarkerStart and treats partial hooks as valid; change the validation to
require both oxHookMarkerStart and oxHookMarkerEnd to be present in the existing
hook content (the variable existing) before returning success, and apply this
same stricter check in both places where the check is performed (the two
occurrences that currently call strings.Contains(existing, oxHookMarkerStart));
if either marker is missing, treat the hook as not installed so the
install/repair path proceeds.

In `@cmd/ox/session_url.go`:
- Around line 12-21: The buildSessionURL function interpolates cfg.RepoID and
sessionName directly into the path; update it to call url.PathEscape on both
values before constructing the final string so path segments are safely escaped
(use url.PathEscape(repoID) and url.PathEscape(sessionName)) and then build the
URL with fmt.Sprintf as before (ensuring you still call
endpoint.NormalizeEndpoint(cfg.GetEndpoint()) and keep the same
"/repo/%s/sessions/%s/view" format).

---

Nitpick comments:
In `@cmd/ox/integrate.go`:
- Around line 219-225: The UX currently always prints "Git commit hooks
installed" even when hooks were already present because InstallGitHooks is
idempotent; call HasGitHooks(gitRoot) first to detect existing hooks and print a
distinct message like "Git commit hooks already installed" when true, and only
call InstallGitHooks when HasGitHooks returns false, handling and logging errors
from InstallGitHooks as before and printing the success message only after a
real install; reference the HasGitHooks and InstallGitHooks functions and the
existing status printing logic for where to make this change.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d21bb8a and 486f70b.

📒 Files selected for processing (16)
  • cmd/ox/agent_prime.go
  • cmd/ox/doctor.go
  • cmd/ox/doctor_git_commit_hooks.go
  • cmd/ox/doctor_types.go
  • cmd/ox/doctor_types_test.go
  • cmd/ox/hooks_cmd.go
  • cmd/ox/hooks_commit_msg.go
  • cmd/ox/hooks_commit_msg_test.go
  • cmd/ox/hooks_git.go
  • cmd/ox/hooks_git_test.go
  • cmd/ox/init.go
  • cmd/ox/integrate.go
  • cmd/ox/session_url.go
  • cmd/ox/session_view_html.go
  • internal/config/attribution.go
  • internal/config/attribution_test.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
cmd/ox/session_url.go (1)

3-8: ⚠️ Potential issue | 🟡 Minor

Escape URL path segments before interpolation.

cfg.RepoID and sessionName are inserted directly into path segments. Escaping both with url.PathEscape avoids malformed links and edge-case path interpretation.

🔧 Proposed fix
 import (
 	"fmt"
+	"net/url"
 
 	"github.com/sageox/ox/internal/config"
 	"github.com/sageox/ox/internal/endpoint"
 )
@@
-	return fmt.Sprintf("%s/repo/%s/sessions/%s/view", ep, cfg.RepoID, sessionName)
+	return fmt.Sprintf(
+		"%s/repo/%s/sessions/%s/view",
+		ep,
+		url.PathEscape(cfg.RepoID),
+		url.PathEscape(sessionName),
+	)
 }
#!/bin/bash
# Verify how session names/repo IDs are sourced and whether they are guaranteed URL-safe.
rg -nP --type=go -C3 '\bGetSessionName\s*\(|\bSessionPath\b|\bRepoID\b'

Also applies to: 22-22

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/ox/session_url.go` around lines 3 - 8, The code injects cfg.RepoID and
sessionName directly into a URL path; modify the function that builds the
session URL (reference symbols cfg.RepoID and sessionName, likely in a function
like SessionPath/BuildSessionURL/GetSessionURL) to call url.PathEscape on both
values before interpolation, add the net/url import, and use the escaped results
when formatting the path so repo IDs and session names with special characters
produce safe, well-formed URLs.
cmd/ox/hooks_git.go (1)

70-71: ⚠️ Potential issue | 🟠 Major

Harden marker validation for install/has/uninstall paths.

Using only the start marker to detect installation can misclassify broken hooks. It also makes uninstall unsafe if the end marker is missing, because removal may consume the rest of the file.

🔧 Proposed hardening
+func hasValidOxHook(content string) bool {
+	return strings.Contains(content, oxHookMarkerStart) &&
+		strings.Contains(content, oxHookMarkerEnd) &&
+		strings.Contains(content, "ox hooks commit-msg")
+}
+
 func InstallGitHooks(gitRoot string) error {
@@
-		if strings.Contains(string(existing), oxHookMarkerStart) {
+		if hasValidOxHook(string(existing)) {
 			return nil // already installed
 		}
@@
-	if !strings.Contains(string(content), oxHookMarkerStart) {
+	if !hasValidOxHook(string(content)) {
 		return nil // ox section not present
 	}
@@
 func HasGitHooks(gitRoot string) bool {
@@
-	return strings.Contains(string(content), oxHookMarkerStart)
+	return hasValidOxHook(string(content))
 }

Also applies to: 108-127, 150-150

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/ox/hooks_git.go` around lines 70 - 71, The current marker check only
looks for oxHookMarkerStart which can mis-detect broken hooks; update all
install/has/uninstall logic to require both oxHookMarkerStart and
oxHookMarkerEnd (or a strict regex that matches the full marker block) before
treating the hook as installed or before removing it. Specifically, replace
occurrences that call strings.Contains(existing, oxHookMarkerStart) with a check
that both oxHookMarkerStart and oxHookMarkerEnd are present (or use a single
compiled regexp matching the entire start..end block) and, in the uninstall
path, refuse to modify the file if the end marker is missing to avoid stripping
the rest of the file. Ensure you reference the oxHookMarkerStart and
oxHookMarkerEnd symbols and update the code paths that currently assume only the
start marker is sufficient.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/ox/hooks_git.go`:
- Around line 64-65: Replace the two uses of os.IsNotExist(err) in the hook-read
checks with errors.Is(err, os.ErrNotExist) (i.e., change the if conditions that
currently read "if err != nil && !os.IsNotExist(err) { ... }" to use
errors.Is(err, os.ErrNotExist)), and add the "errors" package to the imports so
the new checks compile; target the if-conditions that wrap the fmt.Errorf("read
existing hook: %w", err) returns and any equivalent existence checks in the same
file.

---

Duplicate comments:
In `@cmd/ox/hooks_git.go`:
- Around line 70-71: The current marker check only looks for oxHookMarkerStart
which can mis-detect broken hooks; update all install/has/uninstall logic to
require both oxHookMarkerStart and oxHookMarkerEnd (or a strict regex that
matches the full marker block) before treating the hook as installed or before
removing it. Specifically, replace occurrences that call
strings.Contains(existing, oxHookMarkerStart) with a check that both
oxHookMarkerStart and oxHookMarkerEnd are present (or use a single compiled
regexp matching the entire start..end block) and, in the uninstall path, refuse
to modify the file if the end marker is missing to avoid stripping the rest of
the file. Ensure you reference the oxHookMarkerStart and oxHookMarkerEnd symbols
and update the code paths that currently assume only the start marker is
sufficient.

In `@cmd/ox/session_url.go`:
- Around line 3-8: The code injects cfg.RepoID and sessionName directly into a
URL path; modify the function that builds the session URL (reference symbols
cfg.RepoID and sessionName, likely in a function like
SessionPath/BuildSessionURL/GetSessionURL) to call url.PathEscape on both values
before interpolation, add the net/url import, and use the escaped results when
formatting the path so repo IDs and session names with special characters
produce safe, well-formed URLs.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 486f70b and 7d69056.

📒 Files selected for processing (3)
  • cmd/ox/hooks_commit_msg.go
  • cmd/ox/hooks_git.go
  • cmd/ox/session_url.go

Comment on lines +64 to +65
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("read existing hook: %w", err)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the file to check current state at mentioned lines
head -110 cmd/ox/hooks_git.go | tail -50

Repository: sageox/ox

Length of output: 1422


🏁 Script executed:

#!/bin/bash
# Check for all os.IsNotExist occurrences in the file
rg -nP '\bos\.IsNotExist\s*\(' cmd/ox/hooks_git.go

Repository: sageox/ox

Length of output: 125


🏁 Script executed:

#!/bin/bash
# Check current imports in the file
head -20 cmd/ox/hooks_git.go

Repository: sageox/ox

Length of output: 565


Use errors.Is(err, os.ErrNotExist) for consistency with repository error-handling guidelines.

The project standard requires errors.Is() for error checking. Replace both occurrences at lines 64 and 102 in cmd/ox/hooks_git.go with errors.Is(err, os.ErrNotExist) and add the errors import.

♻️ Proposed refactor
 import (
+	"errors"
 	"fmt"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"strings"
 )
@@
-	if err != nil && !os.IsNotExist(err) {
+	if err != nil && !errors.Is(err, os.ErrNotExist) {
 		return fmt.Errorf("read existing hook: %w", err)
 	}
@@
-		if os.IsNotExist(err) {
+		if errors.Is(err, os.ErrNotExist) {
 			return nil // nothing to uninstall
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("read existing hook: %w", err)
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("read existing hook: %w", err)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/ox/hooks_git.go` around lines 64 - 65, Replace the two uses of
os.IsNotExist(err) in the hook-read checks with errors.Is(err, os.ErrNotExist)
(i.e., change the if conditions that currently read "if err != nil &&
!os.IsNotExist(err) { ... }" to use errors.Is(err, os.ErrNotExist)), and add the
"errors" package to the imports so the new checks compile; target the
if-conditions that wrap the fmt.Errorf("read existing hook: %w", err) returns
and any equivalent existence checks in the same file.

@rsnodgrass rsnodgrass merged commit 415e664 into main Mar 3, 2026
1 check passed
@rsnodgrass rsnodgrass deleted the ryan/session-init-work branch March 3, 2026 06:17
rsnodgrass added a commit that referenced this pull request Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant