config

package
v0.17.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 12 Imported by: 0

README

pkg/config

Configuration management for Confab's own config, Claude Code's settings file, and bundled skill file content.

Hook install/uninstall logic lives in pkg/hookconfig. This package owns the generic plumbing — atomic settings updates, settings struct, paths — and the bundled skill templates installed by provider clients.

Files

File Role
config.go ClaudeSettings struct + AtomicUpdateSettings/AtomicUpdateSettingsAt and ReadSettings/ReadSettingsAt (read/modify/write a settings.json with mtime-based optimistic locking). The zero-arg forms target the default (env-resolved) path; the *At(settingsPath, …) forms take an explicit path so hooks can install into a non-default config dir (kata hpec — ClaudeCode.InstallHooks passes p.SettingsPath()). Generic accessor helpers: GetHooksMap, GetEventHooks, SetEventHooks. Tool-name constants used by pkg/hookconfig.
upload.go Confab config: read/write ~/.confab/config.json, validation, default redaction patterns, ParseLogLevel. UploadConfig.Bindings (provider → canonical config dir → {backend_url, api_key}, omitempty) holds per-config-dir backends; only creds vary per binding, redaction/log-level/auto-update stay global. GetUploadConfig is documented default/global only.
binding.go Per-(provider, config dir) backend bindings (kata hpec): Binding, BindingCreds, ResolveBinding(provider, dir, defaultDir) (canonicalizes via pkg/pathcanon; collapses to the default binding when dir == defaultDir), GetUploadConfigFor (merges global fields + binding creds; returns ErrNoBinding for an unbound custom dir — callers must NOT fall back to default), SetBindingCredentials, EnsureAuthenticatedFor, HasBindings.
paths.go Claude state-dir resolution (~/.claude) with CONFAB_CLAUDE_DIR override. ~/.confab paths use pkg/confabpath.
bundled_skills.go Shared bundled-skill registry plus install/uninstall/check and ReconcileBundledSkills (install current + prune retired) helpers for provider-local skills/<name>/SKILL.md layouts
skill_retro.go /retro templates for Claude Code and Codex plus legacy Claude helper wrappers

Two Config Systems

Confab config (~/.confab/config.json)

Managed by upload.go. Contains backend URL, API key, log level, auto-update flag, and redaction settings. This is Confab's own config — we control the schema entirely.

Claude Code settings (~/.claude/settings.json)

Managed by config.go. Contains hooks that Claude Code reads to fire events. We install/uninstall hooks here, but Claude Code owns the file and other tools may write to it concurrently.

Bundled provider skills

Managed by bundled_skills.go and skill_retro.go (and future skill_*.go files). Skills are standalone SKILL.md files installed by provider clients into their local skill layouts: Claude uses ~/.claude/skills/<name>/SKILL.md; Codex uses ~/.codex/skills/<name>/SKILL.md; OpenCode uses ~/.config/opencode/skills/<name>/SKILL.md. If an existing SKILL.md has been customized by the user, install backs it up to SKILL.md.bak before overwriting; if the backup write fails, the install aborts rather than silently overwrite.

Key Types

  • UploadConfig — Confab's configuration (backend URL, API key, redaction settings)
  • ParseLogLevel(string) — translates a config log_level value to logger.Level. Called from pkg/loginit at process startup.
  • ClaudeSettings — Wrapper around map[string]any for Claude Code settings, preserving unknown fields
  • ErrHooksTypeMismatch — Exported sentinel error returned when the "hooks" field in settings.json exists but is not a JSON object. Callers can check errors.Is(err, ErrHooksTypeMismatch) and surface a clear message asking users to fix the file manually.
  • RedactionConfig — Redaction enabled flag, use_default_patterns, custom pattern list
  • RedactionPattern — Individual redaction pattern (name, regex, type, capture group, field pattern)

How to Extend

Adding a new Confab config field
  1. Add the field to UploadConfig in upload.go
  2. Add validation in SaveUploadConfig() if needed
  3. Update the setup flow in cmd/setup.go to prompt for / set the field
Adding a new hook type

Hook install/uninstall lives in pkg/hookconfig — see that package's README. The wiring into cmd/ flows through pkg/provider's Provider interface: cmd/hooks.go and cmd/setup.go call p.InstallHooks(), which delegates to hookconfig per provider.

Adding a new bundled skill
  1. Add the provider-rendered template content in skill_<name>.go.
  2. Add the skill name to bundledSkillNames and route it in bundledSkillTemplate (keyed by SkillProviderClaude / SkillProviderCodex / SkillProviderOpencode; providers without a distinct template fall through to the generic one, as OpenCode does for /retro).
  3. Keep path/layout decisions in pkg/provider; pkg/config only receives a state directory and provider name.
  4. Add/update tests for Claude, Codex, and OpenCode installs so all provider paths stay covered.

Invariants

  • Settings writes must use AtomicUpdateSettings(). This provides read-modify-write with mtime-based optimistic locking and exponential backoff retry (max 10 attempts). Never read + write separately — concurrent Claude Code sessions will clobber each other.
  • Config file permissions: 0600 for ~/.confab/config.json (contains API key), 0600 for ~/.claude/settings.json.
  • Directory permissions: 0700 for ~/.confab/ and ~/.claude/ directories created by Confab. Restrictive permissions prevent other users on shared systems from reading config or API keys.
  • GetDefaultRedactionPatterns() pattern order matters. More specific patterns (e.g., sk-ant-api03-...) must come before general ones (e.g., field-name-based patterns) to avoid partial matches.

Design Decisions

ClaudeSettings uses map[string]any instead of typed structs. Claude Code's settings schema evolves rapidly and includes fields we don't manage. A typed struct would silently drop unknown fields on round-trip. The raw map preserves everything.

Mtime-based optimistic locking instead of flock. AtomicUpdateSettings() checks that the file's mtime hasn't changed between read and write. If it has, it retries with backoff. This is simpler than file locking, works cross-platform, and is sufficient for the infrequent writes that hooks installation involves.

Bundled skills use provider-rendered templates. The shipped skills share a registry, but content can differ where the harnesses expose different session IDs or local transcript layouts. ReconcileBundledSkills installs the current bundle and prunes any retired skills (e.g. the removed /til) left by older confab versions.

Testing

go test ./pkg/config/...

Tests cover atomic settings updates under concurrency, field preservation across round-trips, config validation, and bundled skill install/uninstall behavior. Hook install/uninstall tests live in pkg/hookconfig.

Dependencies

Uses: pkg/confabpath (~/.confab path-builder for getConfigPath), pkg/logger (logging from config.go, skill_*.go). paths.go deliberately does not import pkg/provider even though it owns parallel constants — pkg/provider imports pkg/hookconfig, which imports pkg/config. The duplicated ClaudeStateDirEnv constant must stay in sync between the two packages.

Used by: cmd/ (setup, login, hooks, status), pkg/daemon/ (state dir), pkg/hookconfig/ (settings struct, atomic update, tool-name constants), pkg/http/ (upload config), pkg/loginit/ (GetUploadConfig, ParseLogLevel), pkg/provider/ (provider paths, skills install), pkg/redactor/ (redaction patterns), pkg/sync/ (upload config)

Documentation

Overview

ABOUTME: Manages the /retro bundled skill — install and uninstall. ABOUTME: The skill file lives at <provider>/skills/retro/SKILL.md and enables the /retro slash command.

Index

Constants

View Source
const (
	SkillProviderClaude   = "claude-code"
	SkillProviderCodex    = "codex"
	SkillProviderOpencode = "opencode"
	SkillProviderCursor   = "cursor"
)
View Source
const (
	ToolNameBash              = "Bash"
	ToolNameMCPGitHubCreatePR = "mcp__github__create_pull_request"
	// ToolNameCursorShell is Cursor's shell tool name (its equivalent of
	// Bash); both `git commit` and `gh pr create` run through it.
	ToolNameCursorShell = "Shell"
)

Tool names for PreToolUse/PostToolUse hook matching.

View Source
const ClaudeStateDirEnv = "CONFAB_CLAUDE_DIR"

ClaudeStateDirEnv is the environment variable to override the default Claude state directory. Mirrored from pkg/provider; the two must match.

View Source
const DisableLinkFromGitHubEnv = "CONFAB_DISABLE_LINK_FROM_GITHUB"

DisableLinkFromGitHubEnv is the environment variable to disable GitHub linking. When set to any non-empty value, GitHub linking (commits and PRs) is disabled.

Variables

View Source
var ErrHooksTypeMismatch = errors.New("settings.json: 'hooks' field exists but is not a JSON object — please fix manually")

ErrHooksTypeMismatch is returned when the "hooks" field in settings.json exists but is not a JSON object. This prevents silently overwriting user config.

View Source
var ErrNoBinding = errors.New("no confab binding for the requested provider/config dir")

ErrNoBinding is returned by GetUploadConfigFor when a non-default binding has no stored credentials. Callers MUST NOT fall back to the default (top-level) config — doing so would silently sync a custom-dir session to the wrong backend (leak-free policy).

Functions

func AtomicUpdateSettings

func AtomicUpdateSettings(updateFn func(*ClaudeSettings) error) error

AtomicUpdateSettings performs a read-modify-write with optimistic locking. It retries up to maxRetries times if the file is modified by another process. The updateFn receives the current settings and should modify them in-place.

Race condition limitation: The mtime check and rename are not truly atomic. There's a small window (<1ms) between os.Stat() and os.Rename() where another process could modify the file. The retry mechanism mitigates but does not eliminate this race. For most use cases (CLI hook installation, infrequent config changes), the retry logic provides sufficient reliability. If truly atomic updates are required, file locking (flock) would be needed.

func AtomicUpdateSettingsAt added in v0.17.1

func AtomicUpdateSettingsAt(settingsPath string, updateFn func(*ClaudeSettings) error) error

AtomicUpdateSettingsAt is AtomicUpdateSettings against an explicit settingsPath — used to install/uninstall hooks in a non-default config dir (kata hpec). AtomicUpdateSettings is the default-path wrapper.

func BundledSkillNames added in v0.16.0

func BundledSkillNames() []string

BundledSkillNames returns the shipped skill names in install order.

func EnsureDefaultRedaction

func EnsureDefaultRedaction() (bool, error)

EnsureDefaultRedaction ensures the config has a redaction section with defaults. If redaction config already exists (even if disabled), it's left unchanged. Returns true if defaults were added, false if config already had redaction settings.

func GetBinaryPath

func GetBinaryPath() (string, error)

GetBinaryPath returns the absolute path to the confab binary

func GetClaudeStateDir

func GetClaudeStateDir() (string, error)

GetClaudeStateDir returns the Claude state directory path. Defaults to ~/.claude but can be overridden with CONFAB_CLAUDE_DIR.

func GetSettingsPath

func GetSettingsPath() (string, error)

GetSettingsPath returns the path to the Claude settings file (defaults to ~/.claude/settings.json, can be overridden with CONFAB_CLAUDE_DIR).

func HasBindings added in v0.17.1

func HasBindings(provider string) (bool, error)

HasBindings reports whether any non-default bindings exist for the provider. Hook handlers use this as the no-bindings short-circuit: pure single-dir users (no bindings) skip derivation entirely and take the default path.

func InstallBundledSkill added in v0.16.0

func InstallBundledSkill(stateDir, providerName, name string) error

InstallBundledSkill writes one shipped skill to stateDir, backing up a customized existing SKILL.md beside the file before overwriting it. If the backup write fails, the install aborts rather than overwriting user content.

func InstallRetroSkill added in v0.15.0

func InstallRetroSkill() error

InstallRetroSkill writes the /retro skill file to ~/.claude/skills/retro/SKILL.md. If an existing file differs from the template, it is backed up as SKILL.md.bak.

func IsBundledSkillInstalled added in v0.16.0

func IsBundledSkillInstalled(stateDir, name string) bool

func IsLinkFromGitHubDisabled

func IsLinkFromGitHubDisabled() bool

IsLinkFromGitHubDisabled returns true if GitHub linking is disabled via environment variable.

func IsRetroSkillInstalled added in v0.15.0

func IsRetroSkillInstalled() bool

IsRetroSkillInstalled returns true if the /retro skill file exists.

func ParseLogLevel

func ParseLogLevel(level string) (logger.Level, error)

ParseLogLevel parses a log level string and returns the corresponding logger.Level. Empty string defaults to INFO. Unknown values return INFO plus an error.

func ReconcileBundledSkills added in v0.16.2

func ReconcileBundledSkills(stateDir, providerName string) error

ReconcileBundledSkills installs every shipped skill into stateDir and removes any retired skills left over from previous confab versions. It is idempotent: pruning a retired skill that is already absent is a no-op.

func SaveUploadConfig

func SaveUploadConfig(config *UploadConfig) error

SaveUploadConfig writes upload configuration to ~/.confab/config.json

func SetBindingCredentials added in v0.17.1

func SetBindingCredentials(b Binding, backendURL, apiKey string) error

SetBindingCredentials writes backendURL/apiKey to the binding's slot: the top-level fields for the default binding, or Bindings[provider][dir] otherwise. Global fields are preserved.

func SkillPath added in v0.16.0

func SkillPath(stateDir, name string) string

SkillPath returns the provider-local SKILL.md path for name.

func UninstallBundledSkill added in v0.16.0

func UninstallBundledSkill(stateDir, name string) error

func UninstallBundledSkills added in v0.16.0

func UninstallBundledSkills(stateDir string) error

UninstallBundledSkills removes every shipped skill directory from stateDir.

func UninstallRetroSkill added in v0.15.0

func UninstallRetroSkill() error

UninstallRetroSkill removes the /retro skill directory (~/.claude/skills/retro/).

Types

type Binding added in v0.17.1

type Binding struct {
	Provider  string
	Dir       string // canonical config dir; empty for the default binding
	IsDefault bool
}

Binding identifies a (provider, config dir) backend target. The default binding (IsDefault) maps to the top-level config fields for backward compatibility; any other binding maps to Bindings[Provider][Dir].

func ResolveBinding added in v0.17.1

func ResolveBinding(provider, dir, defaultDir string) Binding

ResolveBinding builds the Binding for (provider, dir), treating dir as the default when it is empty or canonically equal to defaultDir. defaultDir is passed in (rather than resolved here) because pkg/config sits below pkg/provider and must not import it.

type BindingCreds added in v0.17.1

type BindingCreds struct {
	BackendURL string `json:"backend_url"`
	APIKey     string `json:"api_key"`
}

BindingCreds holds the backend credentials for one (provider, config dir). Only the credentials vary per binding; redaction/log-level/auto-update are read from the global top-level config.

type ClaudeSettings

type ClaudeSettings struct {
	// contains filtered or unexported fields
}

ClaudeSettings wraps the raw settings map to preserve all fields. This is similar to Python's json.load/json.dump pattern. We intentionally avoid typed structs for hooks since the schema is controlled by Claude Code and evolves rapidly.

func NewClaudeSettings added in v0.16.0

func NewClaudeSettings() *ClaudeSettings

NewClaudeSettings returns an empty ClaudeSettings. Useful for tests and for callers that want to build a settings object before writing.

func ReadSettings

func ReadSettings() (*ClaudeSettings, error)

ReadSettings reads the default Claude settings file, preserving all fields.

func ReadSettingsAt added in v0.17.1

func ReadSettingsAt(settingsPath string) (*ClaudeSettings, error)

ReadSettingsAt reads the Claude settings file at settingsPath, preserving all fields. This is the explicit-path core used to install/inspect hooks in a non-default config dir (kata hpec); ReadSettings is the default-path wrapper.

func (*ClaudeSettings) GetEventHooks added in v0.16.0

func (s *ClaudeSettings) GetEventHooks(eventName string) []any

GetEventHooks returns the array of matchers for an event, as []any. This is a read-only operation that does not create the hooks map if it doesn't exist.

func (*ClaudeSettings) GetHooksMap added in v0.16.0

func (s *ClaudeSettings) GetHooksMap() (map[string]any, error)

GetHooksMap returns the hooks map, creating it if it doesn't exist. Returns an error if the hooks field exists but has the wrong type, to prevent silently overwriting user configuration.

func (*ClaudeSettings) MarshalJSON added in v0.16.0

func (s *ClaudeSettings) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler so callers can inspect the serialized shape directly (used by tests).

func (*ClaudeSettings) SetEventHooks added in v0.16.0

func (s *ClaudeSettings) SetEventHooks(eventName string, matchers []any) error

SetEventHooks sets the array of matchers for an event. If matchers is nil or empty, the event key is removed. If the hooks map becomes empty, it is removed from settings.

type RedactionConfig

type RedactionConfig struct {
	Enabled            bool               `json:"enabled"`
	UseDefaultPatterns *bool              `json:"use_default_patterns,omitempty"` // defaults to true if nil
	Patterns           []RedactionPattern `json:"patterns,omitempty"`
}

RedactionConfig holds redaction settings

func (*RedactionConfig) ShouldUseDefaultPatterns

func (c *RedactionConfig) ShouldUseDefaultPatterns() bool

ShouldUseDefaultPatterns returns true if default patterns should be used. Defaults to true if UseDefaultPatterns is nil.

type RedactionPattern

type RedactionPattern struct {
	Name         string `json:"name"`
	Pattern      string `json:"pattern,omitempty"`
	Type         string `json:"type"`
	CaptureGroup int    `json:"capture_group,omitempty"`
	FieldPattern string `json:"field_pattern,omitempty"`
}

RedactionPattern represents a single redaction pattern

func GetDefaultRedactionPatterns

func GetDefaultRedactionPatterns() []RedactionPattern

GetDefaultRedactionPatterns returns the default high-precision redaction patterns

type UploadConfig

type UploadConfig struct {
	BackendURL string           `json:"backend_url"`
	APIKey     string           `json:"api_key"`
	LogLevel   string           `json:"log_level,omitempty"`   // debug, info, warn, error (default: info)
	AutoUpdate *bool            `json:"auto_update,omitempty"` // nil = enabled (default), false = disabled
	Redaction  *RedactionConfig `json:"redaction,omitempty"`
	// Bindings maps provider -> canonical config dir -> credentials.
	Bindings map[string]map[string]BindingCreds `json:"bindings,omitempty"`
}

UploadConfig holds backend upload configuration.

The top-level BackendURL/APIKey are the DEFAULT binding (the provider's default config dir). Per-(provider, config dir) bindings live under Bindings (kata hpec); only backend_url/api_key vary per binding — Redaction, LogLevel and AutoUpdate stay global. Bindings is omitempty so a pure single-dir install's config.json is byte-identical to before this feature.

func EnsureAuthenticated

func EnsureAuthenticated() (*UploadConfig, error)

EnsureAuthenticated reads the config and verifies it has valid credentials Returns the config if authenticated, or an error if not configured

func EnsureAuthenticatedFor added in v0.17.1

func EnsureAuthenticatedFor(b Binding) (*UploadConfig, error)

EnsureAuthenticatedFor is GetUploadConfigFor plus a credential check, mirroring EnsureAuthenticated for a specific binding.

func GetUploadConfig

func GetUploadConfig() (*UploadConfig, error)

GetUploadConfig reads upload configuration from ~/.confab/config.json.

This returns the DEFAULT/global config (top-level backend_url + api_key). For anything scoped to a specific session or provider config dir, use GetUploadConfigFor(provider.BindingFor(p, configDir)) instead — calling this directly for a custom-config-dir session silently yields the wrong backend (kata hpec).

func GetUploadConfigFor added in v0.17.1

func GetUploadConfigFor(b Binding) (*UploadConfig, error)

GetUploadConfigFor returns the effective UploadConfig for a binding: global fields (redaction, log level, auto-update) from the top-level config, with BackendURL/APIKey from the binding. For the default binding this is exactly GetUploadConfig(). For a non-default binding with no stored credentials it returns ErrNoBinding (callers must not fall back to the default).

func (*UploadConfig) IsAutoUpdateEnabled added in v0.10.2

func (c *UploadConfig) IsAutoUpdateEnabled() bool

IsAutoUpdateEnabled returns whether auto-update is enabled. Defaults to true when AutoUpdate is nil (not set in config).

func (*UploadConfig) Validate

func (c *UploadConfig) Validate() error

Validate checks if the upload config is valid

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL