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
- Variables
- func AllowSet(settings map[string]any) map[string]bool
- func ApplyCodex(w io.Writer, root string, p *CodexPlan, printOnly bool) error
- func ClaudeRules() ([]string, error)
- func ClaudeRulesFor(server string) ([]string, error)
- func CodexHooksDisabled(data []byte) (bool, error)
- func CodexInlineHooks(data []byte) (commands []string, present bool, err error)
- func KeptSuffix(conflicts []string) string
- func LinkReason(link string) string
- func LinkRefusal(rel, link string) error
- func MergeAllow(settings map[string]any, plan *ClaudePlan) error
- func NarrateCodex(w io.Writer, p *CodexPlan, printOnly bool)
- func ReadRegular(root, rel string) ([]byte, fs.FileMode, error)
- func ServerRule(server string) string
- func Summary(states []ClientApproval) string
- func ToolRule(server, tool string) string
- type ClaudeMCPPlan
- type ClaudePlan
- type ClientApproval
- type CodexOptions
- type CodexPlan
- type InlineHook
- type Registration
Constants ¶
const ( ClientClaude = "claude" ClientCodex = "codex" )
Client names, as narrated.
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.
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.
const ClaudeSettings = ".claude/settings.json"
ClaudeSettings is the Claude Code project file that carries the rules.
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.
const CodexServer = "seamark"
CodexServer is the registration name init uses when none exists.
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 ¶
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 ¶
AllowSet returns the string entries of permissions.allow, reading the settings without creating anything.
func ApplyCodex ¶
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 ¶
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 ¶
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
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
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 ¶
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
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
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
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
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 ¶
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.
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 ¶
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.
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.