stacklok / stacklok/mecatl

permissions: make the Claude `settings.json` import reviewable — generate one file, in one lane, with every demotion visible

Open
#413 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

config enhancement
Dominant language
Go
Stars
152
Forks
16
Avg merge
14h 48m
Merged PRs (30d)
536

Description

permissions: make the Claude settings.json import reviewable — generate one file, in one lane, with every demotion visible

Sub-issue of the portability epic (#410) — see the parent for the shared artefact map and inherited
boundaries, including the rule this issue turns on: a *.local source never contributes to a
shared, checked-in destination.

Follow-up to #13 (--import-claude-permissions). Depends on sub-issue A only for where the
result is surfaced.

Problem

--import-claude-permissions translates Claude Code permission rules at runtime and is
lossy by design, fail-safe: a WebFetch(domain:…) ALLOW is demoted to ask, a
Read(~/…) rule is left inert with ~ unexpanded, an unparseable spec is dropped
(cmd/mecated/main.go:1491). Each case is logged — and a log line is not a review artefact.
The team that owns policy cannot sign off on a translation that exists only in a process's
stderr, and the developer cannot see which of their allows quietly became asks.

Why the obvious fix is wrong

"Generate .mecatl/settings.yaml from the Claude file" creates two live sources of truth:
permconfig resolves .claude/settings{,.local}.json and .mecatl/settings{,.local}.yaml
with precedence (resolve.go:29-43). The generated
file would silently win, then go stale the moment anyone edits the Claude side. Worse,
settings.local.json is personal and gitignored — folding it into a checked-in shared
settings.yaml leaks one developer's local allows to the whole repo.

Proposal — one lane, chosen explicitly

mecated migrate --emit-permissions writes one artefact, and the two candidate lanes are
mutually exclusive; pick one in review:

  • (a) .mecatl/settings.local.yaml — the personal, gitignored, higher-precedence lane.
    Correct destination for a settings.local.json translation; becomes live immediately, so
    the runtime import flag should then be redundant for that scope.
  • (b) .mecatl/settings.yaml.proposed (not loaded by permconfig) — a review artefact for
    the shared/checked-in lane. Nothing changes behaviour until a human renames it, which is the
    point: policy lands through a PR.

Preference: (b) for the shared file, (a) for the local file — matching the trust scope of
each source. Never emit both lanes from one source file, and never emit a shared file from a
*.local.json source.

Whichever lane, the generated file carries an inline comment per translated rule:

# from .claude/settings.json: allow WebFetch(domain:example.com)
# DEMOTED to ask — domain-scoped WebFetch allows are not representable (fail-safe)
- match: "WebFetch"
  mode: ask
# from .claude/settings.json: allow Read(~/notes/**)
# INERT — "~" is not expanded; rewrite with an absolute path to activate

Acceptance

  • Emitting from a .claude/settings.json with each lossy class produces a file where every
    rule is traceable to its source rule and every demotion/inert/dropped case is commented with
    its reason.
  • A settings.local.json source never contributes to a shared/checked-in destination
    (asserted by test).
  • The emitted artefact and the runtime --import-claude-permissions path produce the same
    effective decisions
    for the same input — a test drives both and compares, so the visible
    file cannot lie about behaviour.
  • Emit refuses to overwrite an existing destination (ADR 0099's no-overwrite discipline).
  • Project ALLOW rules in the emitted file still require --trust-project to take effect; the
    emit step grants nothing.
  • No secret, env value, or token is copied into the artefact.
  • Docs: docs/usage/permissions-config.md gains the emit flow and the lane rule.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the import path at cmd/mecated/main.go:1491 and the destination precedence in internal/adapter/permconfig/resolve.go. Decide the explicit output lane, then trace each lossy rule through emission and runtime import so both paths produce the same effective decisions. Done means the acceptance cases pass, overwrite is refused, and docs/usage/permissions-config.md describes the flow and lane rule.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
authorization, cli, documentation
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.