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 ¶
- Variables
- func CodexHooksPath(projectDir string) string
- func CodexHooksStanza(projectDir, vaultPath string, p Profile) (string, error)
- func CodexHooksStanzaFor(projectDir, vaultPath string, vaults []string, p Profile) (string, error)
- func CompareInstalled(projectDir string) ([]string, error)
- func CursorHooksPath(projectDir string) string
- func CursorHooksStanza(projectDir, vaultPath string, p Profile) (string, error)
- func CursorHooksStanzaFor(projectDir, vaultPath string, vaults []string, p Profile) (string, error)
- func DetectLegacyHooksJSON(projectDir string) bool
- func MergeStanza(existing []byte, vaultPath string) ([]byte, bool, error)
- func PortableVaultRef(projectDir, vault string, scope Scope) (string, error)
- func PortableVaultRefs(projectDir, vault string, vaults []string, scope Scope) (string, []string, error)
- func RecordableNames() string
- func RemoveStanza(existing []byte) ([]byte, []string, error)
- func ScriptsDir(projectDir string, a Agent) string
- func ScriptsForProfile(p Profile) []string
- func SelectedAgentStanza(home, vault string, vaults []string, p Profile, a Agent, scope Scope, ...) (string, error)
- func SettingsStanza(vaultPath string) (string, error)
- func SettingsStanzaForProfile(p Profile, vaultPath string) (string, error)
- func UserAgentStanza(home, vault string, vaults []string, p Profile, a Agent) (string, error)
- func UserCodexHooksPath() string
- func UserScopeInstalled(home string) bool
- func ValidateRecordable(name string) error
- func WriteDeclaredProfile(projectDir string, p Profile) error
- type Agent
- type BrokenCommand
- type CodexApproval
- type CodexApprovalState
- type CodexHookApproval
- type CursorWiring
- type EventScript
- type EventState
- type EventStatus
- type InstallConfig
- type InstallResult
- type MergeFileResult
- func MergeIntoCodexHooks(projectDir, vaultPath string, p Profile, dryRun bool) (*MergeFileResult, error)
- func MergeIntoCodexHooksFor(projectDir, vaultPath string, vaults []string, p Profile, dryRun bool) (*MergeFileResult, error)
- func MergeIntoCursorHooks(projectDir, vaultPath string, p Profile, dryRun bool) (*MergeFileResult, error)
- func MergeIntoCursorHooksFor(projectDir, vaultPath string, vaults []string, p Profile, dryRun bool) (*MergeFileResult, error)
- func MergeIntoSettings(projectDir, vaultPath string, local, dryRun bool) (*MergeFileResult, error)
- func MergeIntoSettingsFor(projectDir, vaultPath string, vaults []string, local, dryRun bool) (*MergeFileResult, error)
- func MergeIntoSettingsForProfile(projectDir, vaultPath string, vaults []string, p Profile, local, dryRun bool) (*MergeFileResult, error)
- func MergeSelectedAgentHooks(home, vault string, vaults []string, p Profile, a Agent, scope Scope, ...) (*MergeFileResult, error)
- func MergeUserAgentHooks(home, vault string, vaults []string, p Profile, a Agent, dryRun bool) (*MergeFileResult, error)
- type Profile
- type ProvisionResult
- type RemoveFileResult
- type Scope
- type ScopeOverlap
- type ScriptState
- type ScriptStatus
- type StatusReport
Constants ¶
This section is empty.
Variables ¶
var RecordableEvents = map[string]string{
"write_prompt": "the PreCompact write-path prompt was shown to the agent",
}
RecordableEvents is the closed set of event names `vaultmind hooks record` accepts.
Deliberately an allowlist rather than a free-text field. The usage log is the evidence base for every retrieval measurement, and #121 has just finished closing one route by which non-agent activity got written into it. A general "record any event" CLI would reopen the same hole from the other side: any script, test, or typo could inject rows that later read as agent behaviour.
A hook that wants a new event name adds it here, in a diff someone reviews.
Functions ¶
func CodexHooksPath ¶ added in v0.8.0
CodexHooksPath is where the Codex wiring for projectDir lives.
func CodexHooksStanza ¶ added in v0.8.0
CodexHooksStanza renders .codex/hooks.json for projectDir.
func CodexHooksStanzaFor ¶ added in v0.8.0
CodexHooksStanzaFor is CodexHooksStanza with an optional federation.
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. Implemented on top of Status so "what counts as drift" is decided in exactly one place — this view and `hooks status` disagreeing would be worse than either being wrong alone, since doctor reports one and the operator acts on the other.
func CursorHooksPath ¶ added in v0.10.2
CursorHooksPath is where the Cursor wiring for projectDir lives.
func CursorHooksStanza ¶ added in v0.10.2
CursorHooksStanza renders .cursor/hooks.json for projectDir.
func CursorHooksStanzaFor ¶ added in v0.10.2
CursorHooksStanzaFor is CursorHooksStanza with an optional federation.
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 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 PortableVaultRef ¶ added in v0.10.17
PortableVaultRef is the vault path hooks install bakes into a command. Project scope writes $CLAUDE_PROJECT_DIR/<rel> when vault is inside projectDir. A vault outside the project, and every user-scope vault, stays absolute: there is no single project to be relative to, and a hook's working directory is not the directory the flag was typed in. Symlinks are not evaluated. The path that was named is the path a teammate's checkout has.
func PortableVaultRefs ¶ added in v0.10.17
func PortableVaultRefs(projectDir, vault string, vaults []string, scope Scope) (string, []string, error)
PortableVaultRefs applies PortableVaultRef to a primary vault and a --vaults list.
func RecordableNames ¶ added in v0.7.1
func RecordableNames() string
RecordableNames lists the accepted names, sorted, for error messages and help.
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 ScriptsDir ¶ added in v0.9.1
ScriptsDir is where the hook scripts for agent a live in projectDir.
func ScriptsForProfile ¶ added in v0.8.0
ScriptsForProfile returns the script names a profile expects to be present and wired. Anything outside the set is neither missing nor unwired for this adopter — it is not part of what they run.
func SelectedAgentStanza ¶ added in v0.10.9
func SelectedAgentStanza(home, vault string, vaults []string, p Profile, a Agent, scope Scope, only []string) (string, error)
SelectedAgentStanza renders only the scripts requested by --only.
func SettingsStanza ¶
func SettingsStanzaForProfile ¶ added in v0.8.0
SettingsStanzaForProfile renders only the hooks the declared profile runs, so a knowledge vault is not wired for a persona it does not have.
func UserAgentStanza ¶ added in v0.10.9
UserAgentStanza renders a verified user hook file. Codex runs from session cwd (https://learn.chatgpt.com/docs/hooks); Cursor's adapter gets the project from workspace_roots (https://cursor.com/docs/hooks).
func UserCodexHooksPath ¶ added in v0.10.9
func UserCodexHooksPath() string
UserCodexHooksPath honors CODEX_HOME, like Codex's own config discovery.
func UserScopeInstalled ¶ added in v0.10.9
UserScopeInstalled notices legacy installs too, without declaring them healthy or changing the project's own status/exit code.
func ValidateRecordable ¶ added in v0.7.1
ValidateRecordable reports whether name is an accepted hook event.
func WriteDeclaredProfile ¶ added in v0.8.0
WriteDeclaredProfile records the choice for a Claude Code install. Callers pass a validated Profile.
Types ¶
type Agent ¶ added in v0.9.1
type Agent string
Agent is the coding agent a project's hooks are installed for.
type BrokenCommand ¶ added in v0.10.9
type BrokenCommand struct {
Agent Agent `json:"agent"`
Event string `json:"event"`
Command string `json:"command"`
SettingsPath string `json:"settings_path"`
Reason string `json:"reason"`
Fix string `json:"fix"`
}
BrokenCommand reports the actual wiring, independently of installed copies.
type CodexApproval ¶ added in v0.9.1
type CodexApproval struct {
HooksFile string `json:"hooks_file"`
Hooks []CodexHookApproval `json:"hooks"`
// ScriptsDir is where Codex runs the scripts from (.vaultmind/scripts);
// Scripts compares each wired script there with the canonical copy.
ScriptsDir string `json:"scripts_dir"`
Scripts []ScriptStatus `json:"scripts"`
}
CodexApproval reports the approvals for a project's Codex hooks.
func (*CodexApproval) Approved ¶ added in v0.9.1
func (a *CodexApproval) Approved() int
Approved counts hooks Codex will run.
func (*CodexApproval) ScriptCounts ¶ added in v0.9.1
func (a *CodexApproval) ScriptCounts() (inSync, drifted, missing int)
ScriptCounts tallies the Codex scripts' states.
func (*CodexApproval) Unapproved ¶ added in v0.9.1
func (a *CodexApproval) Unapproved() int
Unapproved counts hooks Codex will skip — never approved, or disabled.
type CodexApprovalState ¶ added in v0.9.1
type CodexApprovalState string
CodexApprovalState is what Codex has recorded for one VaultMind hook.
const ( CodexApproved CodexApprovalState = "approved" CodexNotApproved CodexApprovalState = "not_approved" CodexDisabled CodexApprovalState = "disabled" // CodexModified: approved once, changed since (e.g. an upgrade rewrote the // command). Codex shows it as "modified" in /hooks and skips it. CodexModified CodexApprovalState = "modified" )
Approval states.
type CodexHookApproval ¶ added in v0.9.1
type CodexHookApproval struct {
Event string `json:"event"`
Script string `json:"script"`
State CodexApprovalState `json:"state"`
// Key and Hash are Codex's own names for this hook's approval: the
// [hooks.state."<key>"] entry, and the trusted_hash it must carry.
Key string `json:"key"`
Hash string `json:"hash"`
}
CodexHookApproval is one VaultMind hook in .codex/hooks.json.
type CursorWiring ¶ added in v0.10.2
type CursorWiring struct {
HooksPath string `json:"hooks_path"`
ScriptsDir string `json:"scripts_dir"`
Wired []string `json:"wired"`
Scripts []ScriptStatus `json:"scripts"`
}
CursorWiring is what status reports for Cursor: the scripts its hooks.json runs and the state of each copy, so a Cursor project is judged on what it runs — not on Claude Code scripts it never had.
func (CursorWiring) ScriptCounts ¶ added in v0.10.2
func (c CursorWiring) ScriptCounts() (inSync, drifted, missing int)
ScriptCounts returns how many of Cursor's scripts are in each state.
type EventScript ¶ added in v0.7.1
EventScript is one canonical event→script pair, exported so tests and callers can build or check a project's wiring without duplicating the map.
func CanonicalEventScripts ¶ added in v0.7.1
func CanonicalEventScripts() []EventScript
CanonicalEventScripts lists the event→script pairs this binary wires.
func EventScriptsForProfile ¶ added in v0.8.0
func EventScriptsForProfile(p Profile) []EventScript
EventScriptsForProfile filters the canonical event map the same way, so the wiring check and the script check cannot disagree about what this adopter signed up for.
type EventState ¶ added in v0.7.1
type EventState string
EventState says whether a canonical event is actually wired in the project's settings.json.
const ( // EventWired — settings.json runs this canonical script on this event. EventWired EventState = "wired" // EventUnwired — the binary wires this event and the project does not. // Reported separately because an absence that renders as nothing looks // exactly like health: the whole reason this command exists, applied to the // event map rather than to the script files. EventUnwired EventState = "unwired" // EventStaleMatcher — the script is wired, but under a matcher an earlier // release installed, so it misses tools it now needs to see (the reach hook // on "Bash" cannot see Edit or Write). `hooks install --merge` upgrades it. EventStaleMatcher EventState = "stale_matcher" // EventOutsideProfile — the project runs one of our hooks that its declared // profile excludes (a persona loader in a knowledge project). It runs a // script the profile chose not to have; `hooks install --profile <p> // --merge` removes it. EventOutsideProfile EventState = "outside_profile" )
type EventStatus ¶ added in v0.7.1
type EventStatus struct {
Event string `json:"event"`
Script string `json:"script"`
State EventState `json:"state"`
}
EventStatus is one canonical event→script pair and whether it is live.
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
// ScopeUser treats ProjectDir as HOME and uses absolute script references.
Scope Scope
// 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
// Vaults, when set, federates the SEARCHING hooks (recall, decision-time
// reach) across every listed vault via VAULTMIND_VAULTS. The persona loader
// still reads one vault: VaultPath, else the first listed. Empty = today's
// single-vault wiring, byte for byte.
Vaults []string
// Profile is the capability profile to install and DECLARE. Empty means
// ProfileFull, so every existing caller is unaffected. Declaring it is the
// point: without a record of what the adopter chose, `hooks status` has
// nothing to judge against and goes back to grading every project on the
// binary's whole inventory — which reported deliberate omissions as
// defects on every run.
Profile Profile
// Agent decides where the scripts go: .claude/scripts for Claude Code (the
// default, empty), .vaultmind/scripts for Codex.
Agent Agent
// DryRun reports what would be written without touching the disk: no
// scripts, no scripts dir, no declared profile. Written then lists the
// scripts that would be written.
DryRun bool
}
InstallConfig controls a hooks-install run.
type InstallResult ¶
type InstallResult struct {
ProjectDir string `json:"project_dir"`
ScriptsDir string `json:"scripts_dir"`
DryRun bool `json:"dry_run,omitempty"`
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"`
// Removed names our hooks taken out because they fall outside the
// profile being installed (sorted, unique; empty when none).
Removed []string `json:"removed,omitempty"`
Overlaps []ScopeOverlap `json:"overlaps,omitempty"`
}
MergeFileResult reports the outcome of MergeIntoSettings.
func MergeIntoCodexHooks ¶ added in v0.8.0
func MergeIntoCodexHooks(projectDir, vaultPath string, p Profile, dryRun bool) (*MergeFileResult, error)
MergeIntoCodexHooks additively merges VaultMind's Codex wiring into <projectDir>/.codex/hooks.json — the same rules as the Claude Code merge: existing hooks preserved, re-runs change nothing, malformed files refused.
func MergeIntoCodexHooksFor ¶ added in v0.8.0
func MergeIntoCodexHooksFor(projectDir, vaultPath string, vaults []string, p Profile, dryRun bool) (*MergeFileResult, error)
MergeIntoCodexHooksFor is MergeIntoCodexHooks with an optional federation.
func MergeIntoCursorHooks ¶ added in v0.10.2
func MergeIntoCursorHooks(projectDir, vaultPath string, p Profile, dryRun bool) (*MergeFileResult, error)
MergeIntoCursorHooks merges VaultMind's Cursor wiring into <projectDir>/.cursor/hooks.json: the user's hooks kept, our own stale entries replaced, re-runs changing nothing, a malformed file refused.
func MergeIntoCursorHooksFor ¶ added in v0.10.2
func MergeIntoCursorHooksFor(projectDir, vaultPath string, vaults []string, p Profile, dryRun bool) (*MergeFileResult, error)
MergeIntoCursorHooksFor is MergeIntoCursorHooks with an optional federation.
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 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.
func MergeIntoSettingsFor ¶ added in v0.8.0
func MergeIntoSettingsFor(projectDir, vaultPath string, vaults []string, local, dryRun bool) (*MergeFileResult, error)
MergeIntoSettingsFor is MergeIntoSettings with an optional federation: the searching hooks are wired to every vault in vaults.
func MergeIntoSettingsForProfile ¶ added in v0.9.5
func MergeIntoSettingsForProfile(projectDir, vaultPath string, vaults []string, p Profile, local, dryRun bool) (*MergeFileResult, error)
MergeIntoSettingsForProfile wires only the hooks profile p runs. Without the filter a knowledge install wired persona loading and episode capture to scripts the profile had just declined to write (the Codex path already filtered; this one did not).
func MergeSelectedAgentHooks ¶ added in v0.10.9
func MergeSelectedAgentHooks(home, vault string, vaults []string, p Profile, a Agent, scope Scope, dryRun bool, only []string) (*MergeFileResult, error)
MergeSelectedAgentHooks wires only the requested scripts, preserving other entries; user commands are migrated only when their shape is generated.
func MergeUserAgentHooks ¶ added in v0.10.9
func MergeUserAgentHooks(home, vault string, vaults []string, p Profile, a Agent, dryRun bool) (*MergeFileResult, error)
MergeUserAgentHooks uses the same preservation/idempotency rules as the corresponding project install, targeting the harness's user config.
type Profile ¶ added in v0.8.0
type Profile string
Capability profiles: what this adopter CHOSE to run, so status judges the choice rather than the binary's full inventory.
Why this exists. `hooks status` compared every project against every script the binary ships. Run against focalc's knowledge vault — correctly configured, deliberately without persona, episodes or mesh — it reported "3 unwired, 1 missing" on every single run. A report that is wrong every time is not a strict check; it is a channel the reader learns to skip, and then it is wrong on the day it is right.
DECLARED, never inferred. Deriving the profile from what happens to be installed would make the check vacuous — nothing could ever be missing, because whatever is present would define the expectation. Same rule the mesh daemon URL and the registry staleness bound already follow: a guessed value is worse than none, because it is confidently wrong.
const ( // ProfileFull is every canonical hook: persona, episodes, mesh, recall. // It is the default when nothing is declared, so existing adopters see // exactly today's behaviour. ProfileFull Profile = "full" // ProfileKnowledge is a curated knowledge vault: recall, decision-time // reach, read tracking and health. No persona to load, no transcript to // capture, no mesh to watch. (focalc's shape, and the first adopter this // tool was actively wrong about.) ProfileKnowledge Profile = "knowledge" // ProfilePersona is an agent identity vault without the mesh: persona, // episodes and recall, but no wake-watcher. ProfilePersona Profile = "persona" )
func DeclaredProfile ¶ added in v0.8.0
DeclaredProfile reads the project's declared profile. Absent ⇒ ProfileFull, preserving today's behaviour for every existing adopter. An unrecognised value is an ERROR: silently treating a typo as "full" would report a misconfigured project as a healthy one.
A Claude Code install declares it under .claude/, a Codex install under .vaultmind/; the Claude Code location is read first.
func DeclaredProfileIfAny ¶ added in v0.10.0
DeclaredProfileIfAny reads the project's declared profile and reports whether there was one. DeclaredProfile answers "full" for both a project that declared full and one that declared nothing; a caller that must tell them apart (init choosing hooks for the vault it just made) uses this.
func InstallProfile ¶ added in v0.10.0
InstallProfile is the profile a bare `hooks install` uses: the declared one if any; else full when hooks are already installed (every install before profiles existed was full and declared nothing, and the upgrade command must not strip their persona hooks); else knowledge, matching `init`.
"Already installed" means a scripts dir exists. An undeclared project whose scripts dir was deleted by hand while settings still wire the hooks reads as fresh, and a knowledge --merge then unwires its persona hooks; `hooks uninstall` removes both halves, so only a manual deletion lands there.
func InstallProfileForScope ¶ added in v0.10.9
InstallProfileForScope keeps user declarations independent from a project install made with HOME as its root. Legacy undeclared user scripts are full.
func ParseProfile ¶ added in v0.8.0
ParseProfile validates an operator-supplied profile name. Empty means ProfileFull. An unrecognised value is an error rather than a fallback: a typo that silently installs the full set is the failure this feature exists to remove.
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"`
// ScriptsKept names the scripts --remove-scripts left in place because the
// other agent sharing .vaultmind/scripts still runs them.
ScriptsKept []string `json:"scripts_kept,omitempty"`
}
RemoveFileResult reports the outcome of RemoveFromSettings.
func RemoveFromCodexHooks ¶ added in v0.9.3
func RemoveFromCodexHooks(projectDir string, removeScripts bool) (*RemoveFileResult, error)
RemoveFromCodexHooks is RemoveFromSettings for Codex: it strips VaultMind's entries from .codex/hooks.json (the same {"hooks": {...}} shape, so the same surgical removal) and, with removeScripts, deletes the scripts from .vaultmind/scripts. It never removes the .vaultmind folder itself — that can also be a vault's own data folder — nor touches Codex's approval records.
func RemoveFromCursorHooks ¶ added in v0.10.2
func RemoveFromCursorHooks(projectDir string, removeScripts bool) (*RemoveFileResult, error)
RemoveFromCursorHooks strips VaultMind's entries from .cursor/hooks.json, keeping the user's, and with removeScripts deletes the scripts from .vaultmind/scripts unless Codex still runs them.
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.
type Scope ¶ added in v0.10.9
type Scope string
Scope selects a project's hooks or the user's hooks across projects.
func ParseScope ¶ added in v0.10.9
ParseScope accepts the two supported scopes. Empty keeps project defaults.
type ScopeOverlap ¶ added in v0.10.9
type ScopeOverlap struct {
Agent Agent `json:"agent"`
Event string `json:"event"`
Scripts []string `json:"scripts"`
UserSettings string `json:"user_settings"`
ProjectSettings string `json:"project_settings"`
}
ScopeOverlap names the scripts an agent dispatches from both scopes on an event.
func ScopeOverlaps ¶ added in v0.10.9
func ScopeOverlaps(project, home string, preview *MergeFileResult, agentFilter Agent) ([]ScopeOverlap, error)
ScopeOverlaps compares actual wiring, including project-local Claude settings. An optional preview substitutes just the file being merged, so dry runs warn about the wiring they would create without writing it first.
type ScriptState ¶ added in v0.6.0
type ScriptState string
ScriptState is what a project's copy of a canonical hook script is doing relative to the one embedded in this binary.
const ( // ScriptInSync means the installed copy behaves identically to the // canonical one. Comments and blank lines are ignored — a hook's behaviour // is its code, and treating annotations as drift makes the whole signal // unusable, which is how a drift report teaches people to ignore it. ScriptInSync ScriptState = "in_sync" // ScriptDrifted means the installed copy differs behaviourally. Either the // project changed it (and the change should go upstream to survive an // update) or the binary moved ahead (and the project should update). ScriptDrifted ScriptState = "drifted" // ScriptMissing means the binary ships this hook and the project does not // have it. Distinct from in-sync for a reason: two hooks lived only in one // consumer for months while every adopter silently lacked them, and no // command would say so — absence reported as nothing looks like health. ScriptMissing ScriptState = "missing" )
type ScriptStatus ¶ added in v0.6.0
type ScriptStatus struct {
Name string `json:"name"`
State ScriptState `json:"state"`
}
ScriptStatus is one canonical script's state in a project.
type StatusReport ¶ added in v0.6.0
type StatusReport struct {
ProjectDir string `json:"project_dir"`
Scope Scope `json:"scope,omitempty"`
ScriptsDir string `json:"scripts_dir,omitempty"`
SettingsPath string `json:"settings_path,omitempty"`
UserScopeInstalled bool `json:"user_scope_installed,omitempty"`
Overlaps []ScopeOverlap `json:"overlaps,omitempty"`
BrokenCommands []BrokenCommand `json:"broken_commands,omitempty"`
Installed bool `json:"installed"`
Scripts []ScriptStatus `json:"scripts"`
// Events reports whether each canonical event is wired in settings.json.
// Script contents and event wiring are independent failures: a project can
// hold every script byte-identical and still run none of them.
Events []EventStatus `json:"events"`
// Profile is the DECLARED capability profile this report judged against.
// Emitted so a reader can tell "healthy" from "healthy for a narrower
// profile than you thought you installed".
Profile Profile `json:"profile"`
// Codex reports whether Codex has approved each VaultMind hook in
// .codex/hooks.json; nil when the project has none. Codex skips an
// unapproved hook silently, so this is the only place it shows.
Codex *CodexApproval `json:"codex,omitempty"`
// Cursor reports the scripts .cursor/hooks.json runs and their state; nil
// when the project has no Cursor wiring of ours.
Cursor *CursorWiring `json:"cursor,omitempty"`
// LeftoverClaudeScripts is set when a Codex project with no Claude Code
// wiring still has .claude/scripts — left by an install from before Codex
// scripts moved to .vaultmind/scripts. Reported, never deleted.
LeftoverClaudeScripts string `json:"leftover_claude_scripts,omitempty"`
}
StatusReport is the full comparison between the hooks this binary ships and the ones a project has installed.
func Status ¶ added in v0.6.0
func Status(projectDir string) (StatusReport, error)
Status compares every canonical hook script against the project's installed copy. It is the full picture; CompareInstalled is the drift-only view that doctor consumes.
func StatusForScope ¶ added in v0.10.9
func StatusForScope(projectDir string, scope Scope) (StatusReport, error)
StatusForScope checks scripts and wiring at the selected root (HOME for user).
func (StatusReport) Counts ¶ added in v0.6.0
func (r StatusReport) Counts() (inSync, drifted, missing int)
Counts returns how many scripts are in each state — the summary line.
func (StatusReport) EventCounts ¶ added in v0.7.1
func (r StatusReport) EventCounts() (wired, unwired int)
EventCounts returns how many canonical events are wired and unwired.
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`. |