feat(mcp): ghost mcp init for Claude Code integration#110
Conversation
Adds a one-command setup for Claude Code: - Registers Ghost MCP server via claude CLI - Adds all 13 tool permissions to settings.json - Configures SessionStart hook for context reminders - Imports existing Claude Code memory files into Ghost - Writes project memory redirects pointing to Ghost New subcommands: ghost mcp init, ghost hook session-start New package: internal/mcpinit (init, settings, hook + tests)
📝 WalkthroughWalkthroughThis PR introduces a new Changes
Sequence DiagramsequenceDiagram
actor User
participant Ghost as Ghost CLI
participant MCPInit as mcpinit.Run()
participant Claude as Claude CLI
participant FS as File System
participant GhostDB as Ghost Database
User->>Ghost: ghost mcp init
Ghost->>MCPInit: Execute initialization workflow
MCPInit->>Claude: Check prerequisites (ghost, claude binaries)
Claude-->>MCPInit: ✓ Both found on PATH
MCPInit->>Claude: Query existing MCP server (claude mcp get ghost)
Claude-->>MCPInit: Not registered or config mismatch
MCPInit->>Claude: Register/update MCP server (claude mcp add-json ...)
Claude-->>MCPInit: ✓ Registered
MCPInit->>FS: Load ~/.claude/settings.json
FS-->>MCPInit: Settings loaded
MCPInit->>MCPInit: Add missing ghost permissions (13 tools)
MCPInit->>FS: Save updated settings.json (with .bak backup)
FS-->>MCPInit: ✓ Saved
MCPInit->>FS: Check SessionStart hook in settings
FS-->>MCPInit: Hook not present
MCPInit->>FS: Add SessionStart hook to settings.json
FS-->>MCPInit: ✓ Hook added
MCPInit->>GhostDB: Open ghost.db
GhostDB-->>MCPInit: Database connection
MCPInit->>GhostDB: List projects
GhostDB-->>MCPInit: Project list returned
loop For each project
MCPInit->>MCPInit: Import memories via claudeimport
MCPInit->>FS: Create ~/.claude/projects/<encoded>/memory/ directory
end
MCPInit->>FS: Write MEMORY.md redirects
FS-->>MCPInit: ✓ Redirects written
MCPInit-->>Ghost: ✓ Setup complete
Ghost-->>User: Exit (success)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
cmd/ghost/main.go (1)
457-466: Clarify behavior for unknown hook subcommands.The
runHookfunction silently exits with code 0 when no argument is provided or when an unknown hook type is passed (e.g.,ghost hook unknown). This appears intentional to avoid breaking Claude Code if new hook types are added, but consider adding a debug log or comment to clarify this design choice.💡 Optional: Add a comment explaining the silent fallthrough
// runHook dispatches Claude Code hook events. +// Unknown hook types are silently ignored to maintain forward compatibility +// with future Claude Code hook events. func runHook() { if len(os.Args) < 3 { os.Exit(0) } switch os.Args[2] { case "session-start": mcpinit.HandleSessionStartHook(os.Stdin, os.Stdout) + // Other hook types (e.g., PreToolUse, PostToolUse) are not handled. } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/ghost/main.go` around lines 457 - 466, The runHook function currently returns silently for missing or unrecognized hook subcommands, which is ambiguous; update runHook to explicitly document or log this behavior: inside runHook (function runHook) add a brief comment explaining the intentional silent fallthrough for unknown hooks and, optionally, emit a debug-level log (e.g., using the existing logger) when len(os.Args) < 3 or when the switch hits no case (unknown subcommand) so callers can see why execution exits without error—keep the current exit(0) behavior but make it explicit via comment and/or a debug log surrounding the mcpinit.HandleSessionStartHook call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/mcpinit/hook.go`:
- Around line 14-19: The function HandleSessionStartHook currently ignores the
error returns from fmt.Fprintln (causing errcheck CI failures); update
HandleSessionStartHook to explicitly handle those returns — e.g., capture the
error values from each fmt.Fprintln call and handle or log them (or assign to _
if you intentionally ignore them) and do the same for the io.Copy call if
desired; ensure the error variables are referenced so the linter is satisfied
while preserving the current behavior and keep references to
HandleSessionStartHook and fmt.Fprintln in your change.
In `@internal/mcpinit/init.go`:
- Around line 201-205: Replace the plain defer db.Close() with a deferred
closure that calls db.Close(), checks its returned error, and logs any failure
to the writer passed into importMemories (w) so close errors are not ignored;
locate the db variable opened by memory.OpenDB in init.go and update the defer
to something like a deferred func that invokes db.Close(), captures the error,
and writes a descriptive message including the error to w (use fmt.Fprintf or
fmt.Fprintln) referencing db and importMemories in the message for context.
- Around line 290-296: The MEMORY.md content uses the sanitized display name
(safeName from sanitizeName(p.Name)) for the ghost_project_context project_id
placeholder, which is wrong; update the fmt.Sprintf call that constructs content
(variable content) to keep safeName for the header but pass p.ID (the actual
project identifier) into the "%s" used in the `ghost_project_context`
instruction so the generated command uses the real project ID instead of the
sanitized name.
---
Nitpick comments:
In `@cmd/ghost/main.go`:
- Around line 457-466: The runHook function currently returns silently for
missing or unrecognized hook subcommands, which is ambiguous; update runHook to
explicitly document or log this behavior: inside runHook (function runHook) add
a brief comment explaining the intentional silent fallthrough for unknown hooks
and, optionally, emit a debug-level log (e.g., using the existing logger) when
len(os.Args) < 3 or when the switch hits no case (unknown subcommand) so callers
can see why execution exits without error—keep the current exit(0) behavior but
make it explicit via comment and/or a debug log surrounding the
mcpinit.HandleSessionStartHook call.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 76ce50ce-8a87-4577-96cd-57d9e1351521
📒 Files selected for processing (9)
README.mdcmd/ghost/main.gointernal/claudeimport/import.gointernal/claudeimport/import_test.gointernal/mcpinit/hook.gointernal/mcpinit/init.gointernal/mcpinit/init_test.gointernal/mcpinit/settings.gointernal/mcpinit/settings_test.go
| func HandleSessionStartHook(stdin io.Reader, stdout io.Writer) { | ||
| _, _ = io.Copy(io.Discard, stdin) // drain stdin | ||
| fmt.Fprintln(stdout, "Ghost memory is active. Before starting work:") | ||
| fmt.Fprintln(stdout, "1. Call ghost_project_context with the project name") | ||
| fmt.Fprintln(stdout, "2. Save discoveries with ghost_memory_save during work") | ||
| } |
There was a problem hiding this comment.
Handle fmt.Fprintln return values to fix CI failure.
The pipeline fails due to unchecked error return values from fmt.Fprintln. While stdout writes rarely fail, the linter requires explicit handling.
🔧 Proposed fix to satisfy errcheck
func HandleSessionStartHook(stdin io.Reader, stdout io.Writer) {
_, _ = io.Copy(io.Discard, stdin) // drain stdin
- fmt.Fprintln(stdout, "Ghost memory is active. Before starting work:")
- fmt.Fprintln(stdout, "1. Call ghost_project_context with the project name")
- fmt.Fprintln(stdout, "2. Save discoveries with ghost_memory_save during work")
+ _, _ = fmt.Fprintln(stdout, "Ghost memory is active. Before starting work:")
+ _, _ = fmt.Fprintln(stdout, "1. Call ghost_project_context with the project name")
+ _, _ = fmt.Fprintln(stdout, "2. Save discoveries with ghost_memory_save during work")
}🧰 Tools
🪛 GitHub Actions: ci
[error] 16-16: golangci-lint: Error return value of fmt.Fprintln is not checked (errcheck)
🪛 GitHub Check: build-and-test
[failure] 18-18:
Error return value of fmt.Fprintln is not checked (errcheck)
[failure] 17-17:
Error return value of fmt.Fprintln is not checked (errcheck)
[failure] 16-16:
Error return value of fmt.Fprintln is not checked (errcheck)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/mcpinit/hook.go` around lines 14 - 19, The function
HandleSessionStartHook currently ignores the error returns from fmt.Fprintln
(causing errcheck CI failures); update HandleSessionStartHook to explicitly
handle those returns — e.g., capture the error values from each fmt.Fprintln
call and handle or log them (or assign to _ if you intentionally ignore them)
and do the same for the io.Copy call if desired; ensure the error variables are
referenced so the linter is satisfied while preserving the current behavior and
keep references to HandleSessionStartHook and fmt.Fprintln in your change.
| db, err := memory.OpenDB(dbPath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("open db: %w", err) | ||
| } | ||
| defer db.Close() |
There was a problem hiding this comment.
Handle db.Close() error in defer.
The static analysis correctly flagged that db.Close() error is unchecked. While this is a read-only operation and errors are rare, it's good practice to at least log potential close failures.
Proposed fix
db, err := memory.OpenDB(dbPath)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
- defer db.Close()
+ defer func() {
+ if cerr := db.Close(); cerr != nil {
+ fmt.Fprintf(w, " ! warning: db close: %v\n", cerr)
+ }
+ }()Note: This requires changing the function signature to accept w io.Writer or capturing it in the closure scope. Since w is already passed to importMemories, you can use it directly in the defer closure.
📝 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.
| db, err := memory.OpenDB(dbPath) | |
| if err != nil { | |
| return nil, fmt.Errorf("open db: %w", err) | |
| } | |
| defer db.Close() | |
| db, err := memory.OpenDB(dbPath) | |
| if err != nil { | |
| return nil, fmt.Errorf("open db: %w", err) | |
| } | |
| defer func() { | |
| if cerr := db.Close(); cerr != nil { | |
| fmt.Fprintf(w, " ! warning: db close: %v\n", cerr) | |
| } | |
| }() |
🧰 Tools
🪛 GitHub Check: build-and-test
[failure] 205-205:
Error return value of db.Close is not checked (errcheck)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/mcpinit/init.go` around lines 201 - 205, Replace the plain defer
db.Close() with a deferred closure that calls db.Close(), checks its returned
error, and logs any failure to the writer passed into importMemories (w) so
close errors are not ignored; locate the db variable opened by memory.OpenDB in
init.go and update the defer to something like a deferred func that invokes
db.Close(), captures the error, and writes a descriptive message including the
error to w (use fmt.Fprintf or fmt.Fprintln) referencing db and importMemories
in the message for context.
| safeName := sanitizeName(p.Name) | ||
| content := fmt.Sprintf(`# %s Project Memory | ||
|
|
||
| All project knowledge is stored in Ghost. At session start, run: | ||
| 1. `+"`ghost_list_projects`"+` to discover projects | ||
| 2. `+"`ghost_project_context`"+` with project_id "%s" to load accumulated knowledge | ||
| `, safeName, safeName) |
There was a problem hiding this comment.
Bug: Using sanitized name instead of project ID.
The project_id parameter in the MEMORY.md instructions uses safeName (the sanitized display name) instead of p.ID (the actual project identifier). This will cause ghost_project_context calls to fail since the name won't match any project ID in the database.
Proposed fix
safeName := sanitizeName(p.Name)
content := fmt.Sprintf(`# %s Project Memory
All project knowledge is stored in Ghost. At session start, run:
1. `+"`ghost_list_projects`"+` to discover projects
-2. `+"`ghost_project_context`"+` with project_id "%s" to load accumulated knowledge
-`, safeName, safeName)
+2. `+"`ghost_project_context`"+` with project_id "%s" to load accumulated knowledge
+`, safeName, p.ID)📝 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.
| safeName := sanitizeName(p.Name) | |
| content := fmt.Sprintf(`# %s Project Memory | |
| All project knowledge is stored in Ghost. At session start, run: | |
| 1. `+"`ghost_list_projects`"+` to discover projects | |
| 2. `+"`ghost_project_context`"+` with project_id "%s" to load accumulated knowledge | |
| `, safeName, safeName) | |
| safeName := sanitizeName(p.Name) | |
| content := fmt.Sprintf(`# %s Project Memory | |
| All project knowledge is stored in Ghost. At session start, run: | |
| 1. `+"`ghost_list_projects`"+` to discover projects | |
| 2. `+"`ghost_project_context`"+` with project_id "%s" to load accumulated knowledge | |
| `, safeName, p.ID) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/mcpinit/init.go` around lines 290 - 296, The MEMORY.md content uses
the sanitized display name (safeName from sanitizeName(p.Name)) for the
ghost_project_context project_id placeholder, which is wrong; update the
fmt.Sprintf call that constructs content (variable content) to keep safeName for
the header but pass p.ID (the actual project identifier) into the "%s" used in
the `ghost_project_context` instruction so the generated command uses the real
project ID instead of the sanitized name.
Summary
ghost mcp init— a single command that fully configures Claude Code to use Ghost as its memory systemghost hook session-start— a lightweight SessionStart hook that reminds Claude to load Ghost contextEncodeProjectPathfromclaudeimportpackage for reuseWhat
ghost mcp initdoes (6 steps):ghostandclaudebinaries are on PATHclaude mcp add-jsonmcp__ghost__*tool permissions to~/.claude/settings.jsonSessionStarthook that reminds Claude to callghost_project_contextMEMORY.mdredirect files for all known projectsIdempotent — safe to re-run after updates.
Test plan
go vet ./...cleango test ./...— all pass (15 new tests in mcpinit + existing tests unchanged)go build ./cmd/ghost/compilesghost mcp init— verify output, check settings.json, check MEMORY.md files🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
ghost mcp initcommand for streamlined Claude Code integration setup in a single operationDocumentation