Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ Each agent stores its hook configuration in its own directory. When you run `ent
| Claude Code | `.claude/settings.json` | JSON hooks config |
| Codex | `.codex/hooks.json` | JSON hooks config |
| Copilot CLI | `.github/hooks/entire.json` | JSON hooks config |
| Copilot (VS Code)| `.github/hooks/entire-vscode.json` | JSON hooks config |
| Cursor | `.cursor/hooks.json` | JSON hooks config |
| Factory AI Droid | `.factory/settings.json` | JSON hooks config |
| Gemini CLI | `.gemini/settings.json` | JSON hooks config |
Expand Down Expand Up @@ -447,7 +448,7 @@ Local settings override project settings field-by-field. When you run `entire st

- When enabling Entire for Codex, the command will also create or update `.codex/config.toml` with `codex_hooks = true` to enable Codex hooks. If you configure Codex manually, make sure this flag is set in your `.codex/config.toml`. Or select Codex from the interactive agent picker when running `entire enable`.
- Entire supports Cursor IDE and Cursor Agent CLI tool, but `entire rewind` is not available at this time. Other commands (`doctor`, `status` etc.) work the same as all other agents.
- Entire supports Copilot CLI, but not Copilot in VS Code, in other IDEs, or on github.com.
- Entire supports GitHub Copilot in two places: the Copilot CLI (`.github/hooks/entire.json`) and Copilot in VS Code via VS Code's agent hooks (Preview), which Entire wires up in `.github/hooks/entire-vscode.json`. Both are installed together by `entire enable`. Copilot in other IDEs, or on github.com, is not supported.
- Entire supports Pi coding agent (Preview). Pi uses a TypeScript extension instead of a JSON hook config. Subagent capture is not currently available.

## Security & Privacy
Expand Down
37 changes: 37 additions & 0 deletions cmd/entire/cli/agent/copilotcli/AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,43 @@ The `TranscriptAnalyzer` interface is implemented for Copilot CLI, providing:
- No need for read-modify-write of existing files
- If `entire.json` already exists, read-modify-write to preserve any user additions

## VS Code Agent Hooks (Preview)

`InstallHooks` also writes a second dedicated file, `.github/hooks/entire-vscode.json`,
so Copilot sessions driven from **VS Code's agent hooks (Preview)** are captured.
See `vscode_hooks.go`.

Why a separate file is required: VS Code auto-discovers `.github/hooks/*.json` and
reads Copilot CLI configs by converting lowerCamelCase event names to PascalCase
(`userPromptSubmitted` → `UserPromptSubmitted`, `agentStop` → `AgentStop`). Those
converted names are **not** real VS Code events, so the capture-critical turn hooks
never fire from `entire.json` inside VS Code. The events whose converted names *do*
line up with a real VS Code event (`sessionStart`, `subagentStop`, `preToolUse`,
`postToolUse`) are already delivered by VS Code from `entire.json`, so the VS Code
file only registers the turn events that don't round-trip:

| VS Code event | Entire verb | Lifecycle |
| ------------------ | ------------------------ | --------- |
| `UserPromptSubmit` | `user-prompt-submitted` | TurnStart |
| `Stop` | `agent-stop` | TurnEnd → checkpoint |
| `Stop` | `session-end` | Terminal stop → session end |

VS Code uses a single `Stop` event for both end-of-turn and terminal
session-stop, so both verbs are registered under it. `validateVSCodeEvent`
(`compat.go`) routes each payload to the matching handler by reason and the
non-matching handler no-ops; omitting `session-end` would drop terminal
`SessionEnd` for VS Code-driven sessions. Keeping the file otherwise minimal
avoids double-firing the already-covered events.

VS Code uses the `command` field (not `bash`), a `timeout` field in seconds (not
`timeoutSec`), and has no top-level `version` field — so `entire-vscode.json` is
just `{"hooks": {...}}`. The shared `entire hooks copilot-cli <verb>` handlers
parse both Copilot CLI and VS Code payload shapes (`hookEventName`, ISO-8601
`timestamp`, `transcript_path`) — see `compat.go` and `parseHookEnvelope`.
`entire disable` removes the Entire entries from both files, deleting
`entire-vscode.json` only when no hooks and no user-added top-level fields remain
(user hooks and top-level fields are otherwise preserved on round-trip).

## CLI Flags

- Non-interactive prompt (documented): `copilot -p "prompt" --allow-all-tools`
Expand Down
33 changes: 26 additions & 7 deletions cmd/entire/cli/agent/copilotcli/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,26 @@ var hookConfigKey = map[string]string{
HookNameErrorOccurred: "errorOccurred",
}

// InstallHooks installs Copilot CLI hooks in .github/hooks/entire.json.
// InstallHooks installs Copilot CLI hooks in .github/hooks/entire.json and the
// VS Code-native hook file .github/hooks/entire-vscode.json (see vscode_hooks.go).
// If force is true, removes existing Entire hooks before installing.
// Returns the number of hooks installed.
// Returns the total number of hooks installed across both files.
// Unknown top-level fields and hook types are preserved on round-trip.
func (c *CopilotCLIAgent) InstallHooks(ctx context.Context, localDev bool, force bool) (int, error) {
worktreeRoot, err := paths.WorktreeRoot(ctx)
if err != nil {
worktreeRoot = "."
}

// Install the VS Code-native hook file alongside the Copilot CLI file so
// Copilot sessions run from VS Code's agent hooks (Preview) are captured.
// Its additions count toward the total so a fresh VS Code-file write is
// reported as an install rather than "already installed".
vsCodeCount, err := c.installVSCodeHooks(worktreeRoot, localDev, force)
if err != nil {
return 0, err
}
Comment thread
Soph marked this conversation as resolved.

hooksPath := filepath.Join(worktreeRoot, hooksDir, HooksFileName)

// Use raw maps to preserve unknown fields on round-trip
Expand Down Expand Up @@ -130,7 +140,8 @@ func (c *CopilotCLIAgent) InstallHooks(ctx context.Context, localDev bool, force
}

if count == 0 {
return 0, nil
// No Copilot CLI changes, but the VS Code file may have been updated.
return vsCodeCount, nil
}

// Marshal modified hook types back into rawHooks
Expand Down Expand Up @@ -162,7 +173,7 @@ func (c *CopilotCLIAgent) InstallHooks(ctx context.Context, localDev bool, force
return 0, fmt.Errorf("failed to write %s: %w", HooksFileName, err)
}

return count, nil
return count + vsCodeCount, nil
}

// UninstallHooks removes Entire hooks from Copilot CLI's entire.json.
Expand All @@ -172,6 +183,12 @@ func (c *CopilotCLIAgent) UninstallHooks(ctx context.Context) error {
if err != nil {
worktreeRoot = "."
}

// Remove the VS Code-native hook file's Entire entries too.
if err := c.uninstallVSCodeHooks(worktreeRoot); err != nil {
return err
}

hooksPath := filepath.Join(worktreeRoot, hooksDir, HooksFileName)
data, err := os.ReadFile(hooksPath) //nolint:gosec // path is constructed from repo root + fixed path
if err != nil {
Expand Down Expand Up @@ -244,16 +261,18 @@ func (c *CopilotCLIAgent) AreHooksInstalled(ctx context.Context) bool {
if !errors.Is(err, os.ErrNotExist) {
logging.Warn(ctx, "copilot-cli: failed to read hooks file", "path", hooksPath, "err", err)
}
return false
// The Copilot CLI file may be absent while the VS Code file is present.
return c.areVSCodeHooksInstalled(worktreeRoot)
}

var hooksFile CopilotHooksFile
if err := json.Unmarshal(data, &hooksFile); err != nil {
logging.Warn(ctx, "copilot-cli: failed to parse hooks file", "path", hooksPath, "err", err)
return false
return c.areVSCodeHooksInstalled(worktreeRoot)
}

return hasEntireHook(hooksFile.Hooks.UserPromptSubmitted) ||
return c.areVSCodeHooksInstalled(worktreeRoot) ||
hasEntireHook(hooksFile.Hooks.UserPromptSubmitted) ||
hasEntireHook(hooksFile.Hooks.SessionStart) ||
hasEntireHook(hooksFile.Hooks.AgentStop) ||
hasEntireHook(hooksFile.Hooks.SessionEnd) ||
Expand Down
17 changes: 9 additions & 8 deletions cmd/entire/cli/agent/copilotcli/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ func TestInstallHooks_FreshInstall(t *testing.T) {
t.Fatalf("InstallHooks() error = %v", err)
}

if count != 8 {
t.Errorf("InstallHooks() count = %d, want 8", count)
// 8 Copilot CLI hooks + 3 VS Code hooks (user-prompt-submitted, agent-stop, session-end).
if count != 11 {
t.Errorf("InstallHooks() count = %d, want 11", count)
}

hooksFile := readHooksFile(t, tempDir)
Expand Down Expand Up @@ -87,8 +88,8 @@ func TestInstallHooks_Idempotent(t *testing.T) {
if err != nil {
t.Fatalf("first InstallHooks() error = %v", err)
}
if count1 != 8 {
t.Errorf("first InstallHooks() count = %d, want 8", count1)
if count1 != 11 {
t.Errorf("first InstallHooks() count = %d, want 11", count1)
}

// Second install
Expand Down Expand Up @@ -189,8 +190,8 @@ func TestInstallHooks_ForceReinstall(t *testing.T) {
if err != nil {
t.Fatalf("force InstallHooks() error = %v", err)
}
if count != 8 {
t.Errorf("force InstallHooks() count = %d, want 8", count)
if count != 11 {
t.Errorf("force InstallHooks() count = %d, want 11", count)
}

// Verify no duplicates
Expand Down Expand Up @@ -271,8 +272,8 @@ func TestInstallHooks_PreservesUnknownFields(t *testing.T) {
if err != nil {
t.Fatalf("InstallHooks() error = %v", err)
}
if count != 8 {
t.Errorf("InstallHooks() count = %d, want 8", count)
if count != 11 {
t.Errorf("InstallHooks() count = %d, want 11", count)
}

// Read the raw JSON to verify unknown fields are preserved
Expand Down
Loading
Loading