Help us shape the future of Go! Take the 2026 Go Developer Survey

approve

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package approve manages the configuration that lets a coding agent call the seamark MCP tools without a permission prompt: Claude Code allow rules in .claude/settings.json and Codex per-tool approvals in .codex/config.toml. init writes both on --approve-tools; doctor and status read them. Both files are project configuration: evidence of intent, never a guarantee, because user-level and managed client policy still apply on top.

Index

Constants

View Source
const (
	ClientClaude = "claude"
	ClientCodex  = "codex"
)

Client names, as narrated.

View Source
const (
	StateUnreadable    = "unreadable"
	StateConflicting   = "conflicting"
	StateNotConfigured = "not configured"
	StatePartial       = "partial"
	StateCurrent       = "current"
)

Approval states, from nothing to done. Conflicting and unreadable need a person; partial and not configured need --approve-tools.

View Source
const ClaudeServer = "seamark"

ClaudeServer is the server name the rules use when .mcp.json does not register seamark: the name `claude mcp add seamark` gives it.

View Source
const ClaudeSettings = ".claude/settings.json"

ClaudeSettings is the Claude Code project file that carries the rules.

View Source
const CodexConfig = ".codex/config.toml"

CodexConfig is the project configuration Codex reads for trusted projects; the per-tool approvals and the server registration live in it. Repository-relative, slash-separated.

View Source
const CodexServer = "seamark"

CodexServer is the registration name init uses when none exists.

View Source
const MCPConfig = ".mcp.json"

MCPConfig is the Claude Code project file that registers MCP servers. The server name in it is the prefix of every tool rule, so the rules must be derived from it, never assumed.

Variables

View Source
var Tools = []string{"orient", "why", "change_set", "check", "expand"}

Tools is the seamark MCP tool surface, in definition order. The list lives here rather than in internal/mcp because status imports this package and mcp imports status; a test in internal/mcp pins the two lists to each other.

Functions

func AllowSet

func AllowSet(settings map[string]any) map[string]bool

AllowSet returns the string entries of permissions.allow, reading the settings without creating anything.

func ApplyCodex

func ApplyCodex(w io.Writer, root string, p *CodexPlan, printOnly bool) error

ApplyCodex writes the planned TOML and narrates in init's vocabulary. Every existing byte, comments included, stays where it is: the block is appended, and the two registration keys that complete a header-only table are inserted under that header. The path is checked for links again right before the write, because the plan may be older than the tree.

func ClaudeRules

func ClaudeRules() ([]string, error)

ClaudeRules returns the rules for the conventional server name. The benchmark and the tests use it; init, doctor, and status derive the name from .mcp.json through ClaudeRulesFor.

func ClaudeRulesFor

func ClaudeRulesFor(server string) ([]string, error)

ClaudeRulesFor returns the allow rules --approve-tools merges into .claude/settings.json for one server name: one exact rule per MCP tool and one exact Skill rule per shipped skill. Exact names, never Skill(seamark-*): a wildcard would pre-approve a skill that does not exist yet. The rules do not depend on whether the skills are installed, so an MCP-only setup can approve the tools alone; the benchmark's MCP-only arm needs exactly that.

func CodexHooksDisabled added in v0.7.0

func CodexHooksDisabled(data []byte) (bool, error)

CodexHooksDisabled reports whether .codex/config.toml turns hooks off: `[features] hooks = false`, or its deprecated alias `codex_hooks`. Codex then runs no hook from any source, so an installed seamark hook is inert. The function reads the project file only; the user-level file can carry the flag as well, and the caller must say so.

func CodexInlineHooks added in v0.7.0

func CodexInlineHooks(data []byte) (commands []string, present bool, err error)

CodexInlineHooks reports the hook commands that .codex/config.toml defines inline under hooks. Codex merges the inline hooks with .codex/hooks.json, so a seamark hook in both sources runs twice. present is true when the file has a non-empty hooks table, also when the table holds no command the function can read.

The function reads only. The inline table is the user's, and setup manages the one representation in hooks.json.

func KeptSuffix

func KeptSuffix(conflicts []string) string

KeptSuffix renders the conflicts for an init line, or "" when there are none. init's Claude and Codex lines share it, so the two read alike.

func LinkReason added in v0.8.0

func LinkReason(link string) string

LinkReason is the reason the engine never writes through the link at link. LinkRefusal and the setup engine's write refusal share it, so the text cannot drift between the surfaces that show it.

func LinkRefusal added in v0.7.0

func LinkRefusal(rel, link string) error

LinkRefusal is the error for a client document that setup writes and that is a symbolic link. Setup never writes through a link: a link committed in a cloned repository must not redirect a write outside the tree. Every reader of such a document reports the link with these words.

func MergeAllow added in v0.7.0

func MergeAllow(settings map[string]any, plan *ClaudePlan) error

MergeAllow appends the rules the plan reports missing to permissions.allow, in order. Existing entries, the user's or ours, stay in place. A rule the plan lists as a conflict is never appended: an allow entry cannot override a deny or ask entry, so adding one would only claim what is not so. A present-but-wrong-typed field is an error, not an overwrite, like the hooks merge: setup never clobbers the user's data. init and the Claude Code setup adapter share this merge.

func NarrateCodex added in v0.7.0

func NarrateCodex(w io.Writer, p *CodexPlan, printOnly bool)

NarrateCodex prints the line for one Codex plan in init's vocabulary. ApplyCodex and the Codex setup adapter share it, so the file reads the same whichever path wrote it.

func ReadRegular added in v0.7.0

func ReadRegular(root, rel string) ([]byte, fs.FileMode, error)

ReadRegular reads a client document below root, the rule every reader of a client document follows: the setup readers of the integration package and the records here. It follows a link, and it refuses a path that is not a regular file. A missing file gives an error that matches os.ErrNotExist. The mode is the file's, for a guard.

A read of a FIFO blocks until a writer opens it, so doctor and status would hang where init refuses the file. The function opens the path without blocking and checks the type of the open file. A check of the path before the open leaves a gap: the path can become a FIFO between the check and the open. The flag changes nothing for a regular file.

func ServerRule

func ServerRule(server string) string

ServerRule spells the Claude Code rule that covers every tool of one server. Claude Code documents it beside the per-tool form.

func Summary

func Summary(states []ClientApproval) string

Summary renders the one-line view status prints, for example "claude 8/8 rules · codex not registered". A partial configuration names the corrective command once at the end.

func ToolRule

func ToolRule(server, tool string) string

ToolRule spells the Claude Code permission rule for one MCP tool.

Types

type ClaudeMCPPlan added in v0.7.0

type ClaudeMCPPlan struct {
	// Exists reports whether the file is present.
	Exists bool
	// Server is the registration name: the one the file already holds, or
	// the one the plan adds. Empty when a conflict prevents both.
	Server string
	// Registered is true when the file already registers seamark under
	// Server. Register is true when the plan adds the registration.
	Registered bool
	Register   bool
	// Conflicts lists the entries left alone, described.
	Conflicts []string
	// contains filtered or unexported fields
}

ClaudeMCPPlan is the outcome of inspecting .mcp.json before any write: what is registered, what the plan registers, and what seamark leaves alone.

func PlanClaudeMCP added in v0.7.0

func PlanClaudeMCP(data []byte, exists bool) (*ClaudeMCPPlan, error)

PlanClaudeMCP plans the seamark registration in .mcp.json from bytes the caller already read. exists is false when the file is absent.

The rule for "this server is seamark" is ClaudeRegistration's: the command's base name. A registration under another name is reused, because the allow rules are spelled with that name. An entry named "seamark" that runs another command is a person's choice, so the plan reports it and adds nothing. Malformed JSON and wrong-typed fields are errors: setup must stop before its first write.

The added entry uses the bare command name. The file is committed and shared, so an absolute path of one machine must not enter it.

func (*ClaudeMCPPlan) Document added in v0.7.0

func (p *ClaudeMCPPlan) Document() ([]byte, error)

Document returns the complete file as the plan leaves it, or nil when the plan adds nothing. Keys are written in name order with two-space indentation. HTML escaping is off, so a foreign value that holds "<" or "&" keeps its spelling.

type ClaudePlan

type ClaudePlan struct {
	// Server is the name the tool rules are spelled with.
	Server string
	// Rules is the complete rule set for the server.
	Rules []string
	// Approved lists the rules permissions.allow covers; Missing the
	// rules the merge appends; Conflicts the explicit deny or ask
	// entries that keep a rule from working, described.
	Approved  []string
	Missing   []string
	Conflicts []string
}

ClaudePlan is the outcome of inspecting the permissions in .claude/settings.json for one server name: what is approved, what --approve-tools adds, and what it leaves alone.

func PlanClaude

func PlanClaude(settings map[string]any, server string) (*ClaudePlan, error)

PlanClaude classifies every rule against the three permission lists. Claude Code applies deny, then ask, then allow. A rule in deny or ask is therefore a conflict however allow reads. Appending an allow rule for it would change nothing, and counting it as approved would hide a prompt. The server-wide rule counts for every tool of the server, in each list. An entry that covers several tools is reported once.

func (*ClaudePlan) Conflicting

func (p *ClaudePlan) Conflicting() int

Conflicting counts the rules the conflicts keep from working. It is not len(Conflicts): one server-wide deny entry covers five tool rules.

type ClientApproval

type ClientApproval struct {
	Client string `json:"client"`
	// Path is the repository-relative configuration file.
	Path string `json:"path"`
	// Registered is the server name that runs `seamark mcp`: the
	// .codex/config.toml table for Codex, the .mcp.json entry for Claude
	// Code. Empty when nothing is registered.
	Registered string `json:"registered,omitempty"`
	// Approved counts the rules (Claude Code) or tools (Codex) present;
	// Total is what a complete configuration holds.
	Approved int `json:"approved"`
	Total    int `json:"total"`
	// Conflicts lists explicit settings seamark leaves alone, such as a
	// tool set to prompt, a denied rule, or a server name in use.
	Conflicts []string `json:"conflicts,omitempty"`
	// Err reports a file that cannot be read or parsed.
	Err string `json:"error,omitempty"`
}

ClientApproval summarizes one client's approval configuration for init, doctor, and status.

func Inspect

func Inspect(root string) []ClientApproval

Inspect reports both clients regardless of detection, so a partial configuration in a directory auto would skip stays visible. Errors are recorded on the client, never returned: status must describe a broken setup, not fail on it.

func InspectClaude added in v0.7.0

func InspectClaude(root string) ClientApproval

InspectClaude reports the Claude Code approval configuration alone, for a caller that addresses one client, such as its setup adapter.

func InspectCodex added in v0.7.0

func InspectCodex(root string) ClientApproval

InspectCodex reports the Codex approval configuration alone, for a caller that addresses one client, such as its setup adapter.

func (ClientApproval) Describe

func (c ClientApproval) Describe() string

Describe renders one client in a few words, for example "claude 8/8 rules" or "codex registered as \"seamark\", 5/5 tools approved". init, doctor, and status all print it. A Claude Code server under a name other than the conventional one is named, because the rules are spelled with it. "not configured" is reserved for a client with no registration and no rules, so the words agree with State: a registered client with no rules is partial and reads "claude 0/8 rules", the same way Codex reads "0/5 tools approved".

func (ClientApproval) State

func (c ClientApproval) State() string

State classifies the record by one rule for both clients. A client with no registration and no approvals is not configured, which is information. A registered client with fewer approvals than tools is partial, zero included: a registration alone approves nothing, so every call prompts and the re-run hint is due. Codex approvals without a registration are partial too, because the re-run adds the missing registration.

type CodexOptions added in v0.7.0

type CodexOptions struct {
	// Register adds the `seamark mcp` registration when none exists.
	Register bool
	// Approve adds approval_mode = "approve" for each tool that has no
	// approval and no explicit restriction.
	Approve bool
}

CodexOptions selects what a Codex plan may add to the file. The two operations are separate because registration does not imply approval: explicit client setup registers the server, and the grants stay an opt-in.

type CodexPlan

type CodexPlan struct {
	// Exists reports whether the file is present.
	Exists bool
	// Server is the registration the approvals attach to; empty when a
	// conflict prevents both reuse and registration.
	Server string
	// Registered is true when Server names a registration the file
	// already holds; Register is true when the block adds one. Both are
	// false when the file cannot be extended.
	Registered bool
	Register   bool
	// Missing lists the tools the block approves; Approved the tools
	// already approved, by their own entry or by an approve default.
	Missing  []string
	Approved []string
	// Conflicts lists explicit settings left alone, described.
	Conflicts []string
	// contains filtered or unexported fields
}

CodexPlan is the outcome of inspecting .codex/config.toml before any write: what exists, what the appended block adds, and what seamark leaves alone.

func PlanCodex

func PlanCodex(root string) (*CodexPlan, error)

PlanCodex inspects .codex/config.toml without writing. It rejects a symlinked path, malformed TOML, and wrong-typed or invalid values on the seamark server, because init must fail before its first write and Codex would refuse the file. A conflicting registration or an explicit restrictive setting, a disabled server or a server-wide approval default included, is reported in the plan and left alone, because a person put it there. The appended block is validated by parsing the would-be file, and inline tables are detected from the decoded keys, so a layout that appending cannot extend is reported instead of written. It plans the registration and the approvals together, which is what `init --approve-tools` always meant.

It also rejects a path that is not a regular file, as setup does. A read of a FIFO blocks, and doctor and status must not hang on one.

func PlanCodexData added in v0.7.0

func PlanCodexData(data []byte, exists bool, opts CodexOptions) (*CodexPlan, error)

PlanCodexData plans from bytes the caller already read. The setup coordinator reads the file once under a guard and plans from those same bytes, so the plan and the guard describe one file state. exists is false when the file is absent; data is then ignored. The path checks (symbolic links, file type) are the caller's job.

func (*CodexPlan) Changed added in v0.7.0

func (p *CodexPlan) Changed() bool

Changed reports whether the plan adds anything to the file.

func (*CodexPlan) Document added in v0.7.0

func (p *CodexPlan) Document(existing []byte) []byte

Document returns the complete file as the plan leaves it. existing must be the bytes the plan was made from, because the insert offset refers to them; the setup coordinator's guard enforces that.

type InlineHook added in v0.7.0

type InlineHook struct {
	// Event is the key under [hooks], for example "PreToolUse".
	Event string
	// Matcher is the entry's matcher; empty when the entry has none.
	Matcher string
	// Type is the hook type, for example "command".
	Type    string
	Command string
	// Known is true when the walk read the entry in the documented
	// layout: an array of entries per event, each with an optional
	// matcher and a hooks array. A command found in any other layout has
	// its event and its command only; its matcher and type are unknown,
	// not empty.
	Known bool
}

InlineHook is one hook that .codex/config.toml defines inline under hooks, with the identity Codex reads it by. A duplicate check needs the identity: a seamark gate command under another event, or under a matcher that never fires for the shell tool, runs no gate for shell commands, and the command text alone cannot tell.

func CodexInlineHookEntries added in v0.7.0

func CodexInlineHookEntries(data []byte) (entries []InlineHook, present bool, err error)

CodexInlineHookEntries is CodexInlineHooks with the identity of each hook. The entries are sorted by event, matcher, and command, so a finding is reproducible.

type Registration

type Registration struct {
	// Exists reports whether the file is present.
	Exists bool
	// Server is the name whose command is seamark; empty when none.
	Server string
}

Registration describes what .mcp.json says about seamark.

func ClaudeRegistration

func ClaudeRegistration(root string) (Registration, error)

ClaudeRegistration reads .mcp.json and finds the seamark server by the same basename rule doctor and hooks use: exact "seamark", tolerating the Windows suffix. Several matches prefer the conventional name, then the first in name order, so repeated runs agree. A missing file is not an error; an unparseable one is, because a rule prefix guessed from a broken file would be reported as current while every call prompts. A path that is not a regular file is an error too, as it is in setup.

func (Registration) ServerName

func (r Registration) ServerName() string

ServerName returns the name the tool rules must use: the registered one, or the conventional name when nothing is registered yet.

Jump to

Keyboard shortcuts

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