Documentation
¶
Overview ¶
Package hooks — legacy hooks.json detection.
Claude Code 2.1.129 stopped recognizing the standalone `.claude/hooks.json` file. Projects with that file have silently broken hooks: the JSON is well-formed, the schema is documented, but Claude Code doesn't read it anymore. The fix is to migrate the contents into `.claude/settings.json` under a top-level `hooks` key.
Live evidence: companion project dogfood 2026-05-06/07. SessionStart preread-track sidecar logs prove firing through May 5; absent from May 6 forward. Migration to settings.json restored the layer.
Package hooks implements `vaultmind hooks install` — the command that writes embedded Claude Code hook scripts into a user's project. The embedded source-of-truth lives in internal/hookscripts/; this package is the consumer-facing install path.
SSOT discipline (per a 2026-05-07 design decision ("do it, but keep SSOT, don't drift")): the embedded scripts are the canonical source. Every install copies the SAME bytes. Doctor's hook-drift check (separate work) compares installed copies against the embedded canonical; mismatches surface with `vaultmind hooks install --force` as the resolution.
Index ¶
- func CompareInstalled(projectDir string) ([]string, error)
- func DetectLegacyHooksJSON(projectDir string) bool
- func MergeStanza(existing []byte, vaultPath string) ([]byte, bool, error)
- func RemoveStanza(existing []byte) ([]byte, []string, error)
- func SettingsStanza(vaultPath string) (string, error)
- type InstallConfig
- type InstallResult
- type MergeFileResult
- type ProvisionResult
- type RemoveFileResult
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CompareInstalled ¶
CompareInstalled returns the set of installed-script names whose BEHAVIOR differs from the embedded canonical. The comparison is on the code skeleton (shellparse.StripCommentsAndBlanks): full-line comments and blank lines are ignored, so an installed script that kept richer real-name annotations than the sanitized canonical is NOT flagged, while a real code change is. Useful for doctor's hook-drift check; same shape as DetectContentDrift for vault notes but for hook scripts, and same "only real edits are drift" doctrine.
Names not present in the user's scripts dir are NOT counted as drift — they're "not installed" (a separate signal). Names present but behaviorally matching are not counted (clean state). Only behavioral divergence from canonical counts.
func DetectLegacyHooksJSON ¶
DetectLegacyHooksJSON returns true when `<projectDir>/.claude/hooks.json` exists as a regular file. That's the silent-breakage shape on Claude Code 2.1.129+ — present-but-ignored.
Returns false on: missing file, missing `.claude/` dir, or `hooks.json` existing as a directory rather than a file. Stat errors other than not-exist also return false; doctor is a health summary, not a filesystem-error reporter.
func MergeStanza ¶
MergeStanza additively merges VaultMind's five canonical hook entries into an existing .claude/settings.json (or settings.local.json) byte payload.
The merge is strictly additive and never clobbers: for each of the five events it appends VaultMind's entry only if no entry already in that event's array references the corresponding script (dedup by script basename), so a project's own hooks — and a re-run of this command — are preserved untouched. Top-level keys and their order are kept; only the hooks subtree is rewritten, so the diff a user reviews before committing is minimal.
existing may be empty/whitespace (a fresh file) → the full stanza is written. vaultPath, when non-empty, is baked into each command via VAULTMIND_VAULT (identical to SettingsStanza). Returns changed=false, and the input bytes verbatim, when nothing was appended (idempotent re-run). Malformed existing JSON returns an error and nil output, so a caller never writes a corrupted file over a user's settings.
func RemoveStanza ¶
RemoveStanza strips every hook entry that references a VaultMind-installed script (any name in hookscripts.Names()) from a .claude/settings.json (or settings.local.json) payload — the inverse of MergeStanza. An event array left empty by the removal is dropped, and the hooks object itself is dropped if it becomes empty, so uninstall leaves no orphaned scaffolding. All other content and top-level key order are preserved.
Only entries that reference our canonical scripts are touched: a project's own hooks, and consumer-owned wrappers (e.g. auto-rag-config.sh, which is not one of our script names), are left intact. Returns the sorted, unique set of our script basenames that were removed; an empty set means nothing matched. Returns the input bytes verbatim when there was nothing to remove (idempotent re-run). Malformed JSON returns an error and nil output.
func SettingsStanza ¶
SettingsStanza renders the .claude/settings.json "hooks" object that wires VaultMind's five canonical hooks. The returned string is pretty-printed valid JSON for the operator (or an agent) to merge into their settings.
When vaultPath is non-empty, every command is prefixed with a VAULTMIND_VAULT='<path>' assignment so recall, read-tracking, episode capture, and persona loading all target the consumer's vault instead of the built-in default ($CLAUDE_PROJECT_DIR/vaultmind-identity). The scripts honor VAULTMIND_VAULT as an override; an empty vaultPath leaves the commands unparameterized so they fall back to that default.
Types ¶
type InstallConfig ¶
type InstallConfig struct {
// ProjectDir is the project root. Scripts get written under
// `<ProjectDir>/.claude/scripts/`. If empty, callers should
// resolve to CWD before passing.
ProjectDir string
// Force overwrites existing scripts. Default false (refuse).
Force bool
// Only restricts the install to a subset of canonical scripts
// (by basename, e.g. "auto-rag-guard.sh"). When nil or empty,
// all canonical scripts install. Each name must match an entry
// in hookscripts.Names() — typos are rejected at lint time
// per companion project 2026-05-07 MEDIUM. Use case: consumers who've
// customized some hooks but want a clean canonical install of
// others (the companion project customized vault-track-read + load-persona;
// wants only the auto-RAG slice).
Only []string
// VaultPath, when non-empty, is baked into the emitted settings.json
// stanza (InstallResult.SettingsStanza) via VAULTMIND_VAULT so the
// installed hooks target the consumer's vault instead of the built-in
// vaultmind-identity default. It does not affect which scripts get
// written — only the wiring snippet (issue #41.6).
VaultPath string
}
InstallConfig controls a hooks-install run.
type InstallResult ¶
type InstallResult struct {
ProjectDir string `json:"project_dir"`
ScriptsDir string `json:"scripts_dir"`
Written []string `json:"written"`
Skipped []string `json:"skipped,omitempty"`
Conflicts []string `json:"conflicts,omitempty"`
ForceUsed bool `json:"force_used"`
// SettingsStanza is the .claude/settings.json "hooks" object that
// wires the installed scripts, ready to copy-paste/merge. Populated
// on every successful install so the operator never has to transcribe
// it from the docs (issue #41).
SettingsStanza string `json:"settings_stanza,omitempty"`
}
InstallResult is the JSON-serializable output of Install.
func Install ¶
func Install(cfg InstallConfig) (*InstallResult, error)
Install writes the embedded canonical hook scripts into the configured project's `.claude/scripts/` directory.
Returns InstallResult populated with what was written, skipped (already byte-identical), and conflicts (existed with different content; only relevant when Force=false). When Force=false and there are conflicts, the function writes the non-conflicting scripts AND returns a non-nil error naming the conflicts so the caller can surface them. Force=true skips the conflict check and writes everything.
type MergeFileResult ¶
type MergeFileResult struct {
SettingsPath string `json:"settings_path"`
Changed bool `json:"changed"`
DryRun bool `json:"dry_run"`
// Merged is the post-merge file content — populated so a caller can show a
// dry-run preview or diff. Equal to the on-disk content after a real write.
Merged string `json:"merged,omitempty"`
}
MergeFileResult reports the outcome of MergeIntoSettings.
func MergeIntoSettings ¶
func MergeIntoSettings(projectDir, vaultPath string, local, dryRun bool) (*MergeFileResult, error)
MergeIntoSettings reads the target hook-config file (creating none if it is absent — MergeStanza treats absence as a fresh file), additively merges VaultMind's four canonical hook entries via MergeStanza, and writes the result back. A non-existent file is created with the full stanza. When dryRun is set, nothing is written and the would-be content is returned in MergeFileResult.Merged for preview. A merge that changes nothing writes nothing (idempotent). Any malformed existing settings surfaces as an error before any write, so a user's file is never corrupted.
type ProvisionResult ¶
type ProvisionResult struct {
Install *InstallResult `json:"install"`
Merge *MergeFileResult `json:"merge,omitempty"`
}
ProvisionResult bundles the outcome of installing the hook scripts and (optionally) merging the wiring into a project's settings file. It is the shared return of Provision, used by both `hooks install --merge` and `init --wire-hooks`.
func Provision ¶
func Provision(cfg InstallConfig, merge, local, dryRun bool) (*ProvisionResult, error)
Provision writes the embedded hook scripts into cfg.ProjectDir (Install) and, when merge is true and the install hit no conflict, additively merges the canonical hook wiring into the project's settings file (MergeIntoSettings).
A script conflict (Install returns an error) gates the merge — we never wire settings to point at unresolved scripts — and is returned as the error with Merge left nil. This is the single source of truth for the "install + wire" sequence behind both `hooks install --merge` and `init --wire-hooks`, so the gating and ordering live in exactly one place (manifesto principle 7 — SSOT).
type RemoveFileResult ¶
type RemoveFileResult struct {
SettingsPath string `json:"settings_path"`
Removed []string `json:"removed"`
Changed bool `json:"changed"`
ScriptsDeleted []string `json:"scripts_deleted,omitempty"`
}
RemoveFileResult reports the outcome of RemoveFromSettings.
func RemoveFromSettings ¶
func RemoveFromSettings(projectDir string, local, removeScripts bool) (*RemoveFileResult, error)
RemoveFromSettings strips VaultMind's hook entries from the target hook-config file via RemoveStanza and writes the result back. An absent file is a no-op. When removeScripts is set, the installed canonical scripts under .claude/scripts/ are also deleted. A malformed file surfaces as an error before any write.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package autorag defines the auto-RAG drift catalog schema — the JSON shape consumers use to declare their project-specific drift signatures for `auto-rag-guard.sh`.
|
Package autorag defines the auto-RAG drift catalog schema — the JSON shape consumers use to declare their project-specific drift signatures for `auto-rag-guard.sh`. |