fix(hooks): capture for repos enabled via settings.local.json only · Entire

fix(hooks): capture for repos enabled via settings.local.json only

9e9bde2→main·

suhaanthayyil·3d ago·2 files·+50 added/-2 removed

The #524 gate switched executeAgentHook and the shared git-hook gate to settings.IsSetUpAndEnabled, which required the base .entire/settings.json via IsSetUp. entire enable --local writes only .entire/settings.local.json and never creates the base file, so IsSetUpAndEnabled returned false for those repos before ever loading the local settings — silently no-oping every agent hook and dropping all checkpoint capture for that documented workflow.

Gate on IsSetUpAny (base OR local scope), matching how the rest of the CLI detects setup. The guard is still required so a never-enabled repo is not treated as enabled by Load's default Enabled: true, and any settings read error still fails closed. Fixes the regression for both the agent-hook and git-hook paths.

Changes

2

360 unmodified lines

361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406

360 unmodified lines

"stop hook took %s against a corrupted settings file; want a fast short-circuit, not the transcript-flush-sentinel timeout path", elapsed)
}

// TestExecuteAgentHookCapturesWhenEnabledViaLocalSettingsOnly guards against a
// regression in the #524 fix: `entire enable --local` writes only
// .entire/settings.local.json and never creates the base .entire/settings.json
// (see determineSettingsTarget in setup.go). The disabled-hook gate must
// recognize that local-only enablement — gating on the base file alone
// (settings.IsSetUp) would silently no-op every agent hook for that repo and
// drop all checkpoint capture. Asserted via the same "session was claimed"
// signal (the .agent hint StoreAgentTypeHint writes during SessionStart
dispatch) the short-circuit tests above assert the *absence* of.
func TestExecuteAgentHookCapturesWhenEnabledViaLocalSettingsOnly(t *testing.T) {
    setupStopTestRepo(t)
    repoRoot := mustGetwd(t)

entireDir := filepath.Join(repoRoot, ".entire")
    require.NoError(t, os.MkdirAll(entireDir, 0o750))
    // Local-only enablement: settings.local.json present, base settings.json absent.
    require.NoError(t, os.WriteFile(filepath.Join(entireDir, "settings.local.json"), []byte(`{"enabled":true}`), 0o600))
    require.NoFileExists(t, filepath.Join(entireDir, "settings.json"))

transcriptPath := filepath.Join(repoRoot, "transcript.jsonl")
    require.NoError(t, os.WriteFile(transcriptPath, []byte(`{"type":"user","message":{"content":"hi"}}`+"\n"), 0o600))

sessionID := "local-only-session-start"
    payload, err := json.Marshal(map[string]string{
        "session_id":      sessionID,
        "transcript_path": transcriptPath,
    })
    require.NoError(t, err)

cmd := &cobra.Command{}
    cmd.SetIn(bytes.NewReader(payload))
    cmd.SetErr(&bytes.Buffer{})
    cmd.SetContext(context.Background())

require.NoError(t, executeAgentHook(cmd, agent.AgentNameClaudeCode, claudecode.HookNameSessionStart, false))

hintPath := filepath.Join(repoRoot, ".git", session.SessionStateDirName, sessionID+".agent")
    require.FileExists(t, hintPath, "SessionStart must dispatch and claim the session when Entire is enabled via settings.local.json only")
}

func TestAgentHookPolicyFailsWhenRepoCannotOpen(t *testing.T) {
    _, err := agentHookPolicy(context.Background(), filepath.Join(t.TempDir(), "missing"))

Mcmd/entire/cli/hook_registry_test.go+40

1309 unmodified lines

1310
1311
1312
1313
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1316
1324
1325
1326
1327

1309 unmodified lines

// IsSetUpAndEnabled returns true if Entire is both set up and enabled.
// This checks if .entire/settings.json exists AND has enabled: true.
// "Set up" spans either scope — .entire/settings.json OR
// .entire/settings.local.json — so it must check IsSetUpAny, not IsSetUp.
// `entire enable --local` writes only settings.local.json and never creates the
// base file; gating on the base file alone would treat such a local-only repo
// as inactive and make every hook a silent no-op, dropping all checkpoint
// capture for that documented workflow. The IsSetUpAny guard is still required
// so a never-enabled repo (no settings file in any scope) is not treated as
// enabled by Load's default Enabled: true. Any settings read error is treated
// as disabled (fail closed).
// Use this for hooks that should be no-ops when Entire is not active.
func IsSetUpAndEnabled(ctx context.Context) bool {
    if !IsSetUp(ctx) {
    if !IsSetUpAny(ctx) {
        return false
    }
    s, err := Load(ctx)