From 9c1e036141970667c1a7bb1c6af094d84434b6c7 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Tue, 21 Jul 2026 13:34:30 -0700 Subject: [PATCH] feat(hooks): ship shared plugin hooks and restore hooks docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One hooks/hooks.json at the plugin root serves both agents: SessionStart reports Basecamp auth status as model context, and the paired PreToolUse/PostToolUse Bash hooks drive the HEAD-snapshot commit nudge via the hidden agent-hook command. PostToolUseFailure routes to the same post-commit check so a `git commit && git push` with a failed push still nudges on Claude; Codex ignores unknown event names inside hooks, and its PostToolUse already fires regardless of exit status. No other top-level keys — Codex hard-errors on them. The tool hooks carry no statusMessage: they run on every Bash call and stay visually silent. Commands are plain `basecamp agent-hook ` strings, valid under all four hook shells (sh -lc, cmd.exe /C, Git Bash, PowerShell) with no wrapper or per-agent split. The dormant TestHooksFileCommandsInvokeBasecamp gate activates with this file. Restore the hooks language deferred in cb139726: README and install.md regain hooks in the plugin feature lists and the Codex /hooks trust step (Codex lists untrusted hooks but does not run them until trusted; Claude's install consent covers it), the AGENTS.md tree regains hooks/, the Codex manifest regains the connect-commits clause, and setup codex again points at /hooks. Hooks require a CLI release containing agent-hook. On an older CLI a refreshed plugin payload produces non-blocking unknown-command hook errors on either agent; remediation is `basecamp upgrade`. --- .codex-plugin/plugin.json | 2 +- AGENTS.md | 1 + README.md | 4 +- hooks/hooks.json | 54 ++++++++++++++++++++++++++ install.md | 4 +- internal/commands/wizard_codex.go | 5 ++- internal/commands/wizard_codex_test.go | 11 ++++-- 7 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 hooks/hooks.json diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 5e529908c..597557bc9 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -14,7 +14,7 @@ "interface": { "displayName": "Basecamp", "shortDescription": "Work with Basecamp from Codex.", - "longDescription": "Find assignments, create and update Basecamp work, and diagnose the integration. Requires the Basecamp CLI (basecamp) installed and on your PATH.", + "longDescription": "Find assignments, create and update Basecamp work, diagnose the integration, and connect Git commits to Basecamp todos. Requires the Basecamp CLI (basecamp) installed and on your PATH.", "developerName": "37signals", "category": "Productivity", "capabilities": [ diff --git a/AGENTS.md b/AGENTS.md index ac4765d5c..fde3accf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,6 +35,7 @@ basecamp-cli/ │ └── version/ # Version info ├── e2e/ # BATS integration tests ├── skills/ # Agent skills +├── hooks/ # Agent lifecycle hooks (both agents) ├── .claude-plugin/ # Claude Code integration └── .codex-plugin/ # Codex plugin manifest ``` diff --git a/README.md b/README.md index dadb00e99..8f5153e22 100644 --- a/README.md +++ b/README.md @@ -150,9 +150,9 @@ Both `BASECAMP_OAUTH_CLIENT_ID` and `BASECAMP_OAUTH_CLIENT_SECRET` must be set t Both plugins require the `basecamp` CLI installed and on your PATH. -**Claude Code:** `basecamp setup claude` — installs the plugin with skills and agent workflow support. +**Claude Code:** `basecamp setup claude` — installs the plugin with skills, hooks, and agent workflow support. -**Codex:** `basecamp setup codex` — registers the 37signals marketplace and installs the native plugin with Basecamp skills and diagnostics. Start a new Codex thread afterward to load the skills. +**Codex:** `basecamp setup codex` — registers the 37signals marketplace and installs the native plugin with Basecamp skills, diagnostics, and opt-in hooks. In Codex, review and trust the plugin hooks with `/hooks`, then start a new thread to load the skills and hooks. Manual Codex installation uses the same marketplace: diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 000000000..4b647317a --- /dev/null +++ b/hooks/hooks.json @@ -0,0 +1,54 @@ +{ + "description": "Basecamp agent hooks: session context and commit-reference nudges.", + "hooks": { + "SessionStart": [ + { + "matcher": "startup|resume|clear|compact", + "hooks": [ + { + "type": "command", + "command": "basecamp agent-hook session-start", + "timeout": 5, + "statusMessage": "Checking Basecamp status" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "basecamp agent-hook pre-commit-snapshot", + "timeout": 5 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "basecamp agent-hook post-commit", + "timeout": 5 + } + ] + } + ], + "PostToolUseFailure": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "basecamp agent-hook post-commit", + "timeout": 5 + } + ] + } + ] + } +} diff --git a/install.md b/install.md index 7f4c2d385..c66a712b2 100644 --- a/install.md +++ b/install.md @@ -120,7 +120,7 @@ Both agent plugins require the `basecamp` CLI installed above — the plugin inv basecamp setup claude ``` -This registers the marketplace and installs the plugin with skills and agent workflow support. +This registers the marketplace and installs the plugin with skills, hooks, and agent workflow support. ### Codex @@ -128,7 +128,7 @@ This registers the marketplace and installs the plugin with skills and agent wor basecamp setup codex ``` -This installs the shared Basecamp skill, registers the 37signals Codex marketplace, and installs the native plugin. Start a new Codex thread after setup to load the skills. +This installs the shared Basecamp skill, registers the 37signals Codex marketplace, and installs the native plugin. After setup, review and trust the plugin hooks with `/hooks` (Codex lists untrusted hooks but does not run them until trusted), then start a new Codex thread to load the skills and hooks. For a manual install: diff --git a/internal/commands/wizard_codex.go b/internal/commands/wizard_codex.go index 521cc27b8..314b7ad7f 100644 --- a/internal/commands/wizard_codex.go +++ b/internal/commands/wizard_codex.go @@ -65,7 +65,10 @@ func runCodexSetup(cmd *cobra.Command, styles *tui.Styles) error { fmt.Fprintln(w, styles.RenderStatus(true, "37signals marketplace ready")) fmt.Fprintln(w, styles.RenderStatus(true, "Codex plugin installed and enabled")) fmt.Fprintln(w) - fmt.Fprintln(w, styles.Muted.Render(" Start a new Codex thread to load the Basecamp skills.")) + // Trust must come first: an untrusted SessionStart hook is silently + // skipped, so a thread started before trusting gets no hook context. + fmt.Fprintln(w, styles.Muted.Render(" Review and trust the plugin hooks with /hooks.")) + fmt.Fprintln(w, styles.Muted.Render(" Then start a new Codex thread to load the Basecamp skills.")) return nil } diff --git a/internal/commands/wizard_codex_test.go b/internal/commands/wizard_codex_test.go index 0a3aedb8e..b98f45fec 100644 --- a/internal/commands/wizard_codex_test.go +++ b/internal/commands/wizard_codex_test.go @@ -156,9 +156,14 @@ func TestRunCodexSetupInteractiveExplainsNextSteps(t *testing.T) { assert.Contains(t, output.String(), "Registering 37signals marketplace") assert.Contains(t, output.String(), "Installing basecamp plugin") - assert.Contains(t, output.String(), "Start a new Codex thread") - // No hooks ship yet — setup must not point users at /hooks trust. - assert.NotContains(t, output.String(), "/hooks") + // Codex lists untrusted hooks but does not run them until trusted, and + // an untrusted SessionStart is skipped — so the trust step must come + // before the new-thread step. + trustAt := strings.Index(output.String(), "trust the plugin hooks with /hooks") + newThreadAt := strings.Index(output.String(), "start a new Codex thread") + require.GreaterOrEqual(t, trustAt, 0) + require.GreaterOrEqual(t, newThreadAt, 0) + assert.Less(t, trustAt, newThreadAt, "trust guidance must precede the new-thread step") } func TestRunCodexSetupInteractiveFailureWarnsAndContinues(t *testing.T) {