Skip to content

Add settings plugin: per-plugin TOML config over schema defaults#40

Merged
gering merged 3 commits into
mainfrom
task/plugin-settings-system
Jul 23, 2026
Merged

Add settings plugin: per-plugin TOML config over schema defaults#40
gering merged 3 commits into
mainfrom
task/plugin-settings-system

Conversation

@gering

@gering gering commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • New settings plugin (phase 1: config surface only). Gives the marketplace an explicit, overrideable, validatable config layer so plugins stop hardcoding conventions (tasks/, .claude/worktrees/, task/, …).
  • Ownership model: each plugin owns a schema/settings.schema.json (types, enums, defaults, config filename); the settings plugin owns the infrastructure (discover → resolve → validate → read/write).
  • Defaults reflect current behavior, not a redesign — migrations flip defaults deliberately, later. Consumer wiring (plugins actually reading resolved settings) is a serial follow-up PR.

Changes

  • plugins/settings/scripts/settings.py (Python 3.11+ stdlib, tomllib) + /settings skill. Subcommands: list, show [--overrides], get, set, unset, validate, defaults. show/get return the resolved config by default.
  • Per-plugin schemas: plugins/{work-system,knowledge-system,pr-flow}/schema/settings.schema.json.
  • [related_projects] sibling-project address book (string or {path,role,tags} table form); a missing path warns, never errors.
  • scripts/test_settings.py (run by check-structure.py in CI): defaults, resolve merge, validation (type/enum/unknown/related-projects), TOML round-trip, coercion, CLI set/get/unset cycle.
  • Docs: plugin README (schema format + consumer contract), CHANGELOG, top-level README, CLAUDE.md plugin list, marketplace.json registration.

Readiness

  • ✅ Structure checks (check-structure.py) — 0 errors (2 pre-existing warnings in unrelated files)
  • ✅ settings tests pass; settings validate clean against all three real schemas
  • ✅ Version/CHANGELOG in sync (new plugin at 0.1.0)
  • ✅ Rebased onto latest main
  • ➖ No build step (declarative plugin repo)

Test plan

  • python3 scripts/check-structure.py is green
  • python3 plugins/settings/scripts/settings.py show work-system shows the resolved config
  • settings set work-system.paths.tasks_dir todos writes only the override; unset prunes it
  • settings set knowledge-system.mode.knowledge_mode bogus is rejected (enum)

🤖 Generated with Claude Code

@gering
gering force-pushed the task/plugin-settings-system branch 2 times, most recently from 4dc7fad to a49f086 Compare July 23, 2026 09:24
gering added a commit that referenced this pull request Jul 23, 2026
Codex review of PR #40 found the settings write path follows a symlink at the
config name out of the project root. config_filename() vets the *name*, but the
file on disk is a separate vector: a repo-controlled symlink (a checked-out
`.work-system.toml -> ../../secret`) makes write_text() write outside the root
and read leak the target's contents (a valid-TOML target like ~/.aws/credentials
would surface via `settings show`).

Resolve the config path through a shared `config_path()` helper that rejects a
symlink at the config name — covering both load_user_config (read) and
write_user_config (write/unlink). A settings override is never legitimately a
symlink. Test drives both paths against a valid-TOML symlink target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToUjTdtMa9oXUwjfgERkXL
gering and others added 3 commits July 23, 2026 12:56
Phase 1 (config surface only — consumer wiring is a follow-up):

- New `settings` plugin (v0.1.0) with `scripts/settings.py` (Python 3.11+
  stdlib, tomllib) and the `/settings` skill. Subcommands: list, show
  [--overrides], get, set, unset, validate, defaults. `show` and `get` both
  return the effective (resolved) config by default; `show --overrides` shows
  only what the user changed.
- Ownership model: each plugin ships `schema/settings.schema.json` (a
  JSON-Schema subset carrying types, enums, required, defaults, and the config
  filename). The settings plugin owns discovery, resolve, validate, and IO.
- Schemas for work-system, knowledge-system, pr-flow. Defaults reflect
  *current* behavior (e.g. worktrees_dir = .claude/worktrees), with the neutral
  migration target gated behind [compat] toggles / mode.knowledge_mode — stored
  now, acted on later.
- `[related_projects]` sibling-project address book (string or table entries);
  missing path is a warning, not an error.
- `set`/`unset` write only overrides; defaults stay in the schema. TOML comment
  preservation is deferred (documented).
- Tests in scripts/test_settings.py (run by check-structure.py): defaults,
  resolve merge, validation (type/enum/unknown/related-projects), TOML
  round-trip, coercion, CLI set/get/unset cycle.
- README documents the schema format and the consumer contract; CHANGELOG,
  top-level README, CLAUDE.md plugin list, and marketplace.json updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToUjTdtMa9oXUwjfgERkXL
Address swarm review findings on the settings CLI (all in settings.py):

- TOML string values now escape control chars (\n, \t, … via short forms +
  \uXXXX), so a value with a newline no longer writes a file tomllib refuses to
  reparse (#1).
- dump_toml quotes table-name and key segments that aren't bare-key-safe, so a
  related_projects entry like `web api` / `a.b` survives a rewrite (#7).
- config_filename rejects any x-config-file that isn't a plain basename
  (absolute, `..`, `~`, nested), so a malformed schema can't write/unlink
  outside the project root (#3).
- `set` classifies the target against the schema: setting a section (#5) or
  descending past a scalar leaf (#8) is now a clear error, not a crash or silent
  mis-nest.
- `set` gates the write on schema validity — a coerced array with wrong element
  types (#4) or a mistyped dynamic related_projects field (#6) is refused
  instead of silently writing an invalid config.

Tests: serializer round-trip (control chars + non-bare keys), config-filename
sandbox, and the four set-path guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToUjTdtMa9oXUwjfgERkXL
Codex review of PR #40 found the settings write path follows a symlink at the
config name out of the project root. config_filename() vets the *name*, but the
file on disk is a separate vector: a repo-controlled symlink (a checked-out
`.work-system.toml -> ../../secret`) makes write_text() write outside the root
and read leak the target's contents (a valid-TOML target like ~/.aws/credentials
would surface via `settings show`).

Resolve the config path through a shared `config_path()` helper that rejects a
symlink at the config name — covering both load_user_config (read) and
write_user_config (write/unlink). A settings override is never legitimately a
symlink. Test drives both paths against a valid-TOML symlink target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToUjTdtMa9oXUwjfgERkXL
@gering
gering force-pushed the task/plugin-settings-system branch from 307c756 to abbd8c5 Compare July 23, 2026 10:56
@gering
gering merged commit e43f8e0 into main Jul 23, 2026
1 check passed
@gering
gering deleted the task/plugin-settings-system branch July 23, 2026 12:48
gering added a commit that referenced this pull request Jul 23, 2026
Capture the settings plugin's architecture (merged in PR #40): ownership split
(each plugin owns its schema+defaults, the settings plugin owns resolve/validate/
IO), defaults-encode-current-behavior with [compat] migration, the
[related_projects] peer address book, the consumer contract (read resolved via
settings.py get --json, never the TOML directly), and the serializer/symlink/
set-path hardening lessons from the swarm + Codex reviews. prime: true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ToUjTdtMa9oXUwjfgERkXL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant