ADR-116: Declarative Permission Requirements¶
Context¶
Ways macros and attend sensors both execute shell commands that require tool permissions in ~/.claude/settings.json. Neither tool currently declares what permissions it needs. When a required permission is missing, the tool silently fails or prompts the user mid-session.
The current permission model for way macros uses a flat file (~/.claude/trusted-project-macros) that lists project paths whose macros are allowed to run. This is a binary trust model — a project is either fully trusted or not. It has several problems:
- Opaque — you can't see what a macro will do without reading the script
- All-or-nothing — trusting a project grants all its macros, not specific capabilities
- Siloed — attend sensors have no equivalent mechanism
- Obscure — the file is undiscoverable and undocumented in the tool itself
Meanwhile, settings.json already has a structured permissions.allow list that governs what Claude Code can do. This is the real authority — but there's no way to validate that declared requirements align with granted permissions.
Decision¶
Add a requires: field to both way frontmatter and attend sensor configuration that declares tool permissions. Provide audit commands that diff declared requirements against settings.json grants.
Schema¶
Way frontmatter (new field in frontmatter-schema.yaml):
---
description: GitHub workflow guidance
vocabulary: github pull request merge review
macro: prepend
requires:
- Bash(gh:*)
- Bash(git:*)
---
Attend sensor config (in attend.yaml / config.yaml):
sensors:
+disk-pressure:
script: scripts/disk-check.sh
interval: 60
requires:
- Bash(df:*)
- Bash(du:*)
Built-in sensor defaults — hardcoded in the attend binary, queryable via attend permissions:
| Sensor | Requires |
|---|---|
| git | Bash(git:*) |
| processes | Bash(ps:*) |
| peers | Read |
| context | Read |
Wildcard¶
A way or sensor may declare requires: ["*"] to indicate it needs arbitrary tool access. This is equivalent to the old trusted-project-macros blanket trust — the audit will flag it as "unrestricted" rather than listing specific gaps. This provides a migration path from the old model.
Audit Commands¶
ways permissions audit — scans all way frontmatter (global + project-local), collects requires: fields, diffs against settings.json:
$ ways permissions audit
Way Requires Status
────────────────────────────────────────────────────────
softwaredev/delivery/github Bash(gh:*) MISSING
softwaredev/delivery/github Bash(git:*) granted
softwaredev/code/quality Bash(wc:*) granted
softwaredev/code/quality Bash(find:*) granted
meta/knowledge/authoring Bash(ways:*) granted
1 missing permission. Add to settings.json:
"Bash(gh:*)"
attend permissions audit — same pattern for sensor configs:
$ attend permissions audit
Sensor Requires Status
──────────────────────────────────────
git Bash(git:*) granted
processes Bash(ps:*) MISSING
peers Read granted
+disk-pressure Bash(df:*) granted
+disk-pressure Bash(du:*) MISSING
2 missing permissions. Add to settings.json:
"Bash(ps:*)", "Bash(du:*)"
Permission Format¶
The requires: values use the same permission string format as settings.json:
Read/Read(/path/**)— file read accessWrite(/path/**)— file write accessEdit(/path/**)— file edit accessBash(command:*)— specific bash commandBash(*)— any bash command*— unrestricted (wildcard)
Matching Semantics¶
Permission matching follows a containment hierarchy, not string equality:
*covers everythingBash(*)covers anyBash(command:*)requirementBash(git:*)coversBash(git:status),Bash(git:diff), etc.Read(unscoped) coversRead(/any/path)
The audit checks whether each declared requirement is satisfied by at least one granted permission. A grant of Bash(*) satisfies a requirement of Bash(git:*). A grant of Bash(git:*) does NOT satisfy a requirement of Bash(*).
This matches Claude Code's own permission evaluation — the audit tells you exactly what Claude Code will allow or prompt for.
Config Location¶
Both tools read settings.json from the standard Claude Code location (~/.claude/settings.json). Sensor configs follow the XDG pattern established by attend (ADR-115): user config at $XDG_CONFIG_HOME/attend/config.yaml, project overlay at $PROJECT/.claude/attend.yaml.
Replaces trusted-project-macros¶
The trusted-project-macros file is deprecated. Project-local ways that declare requires: are validated against settings.json like any other way. A project-local way with requires: ["*"] is equivalent to the old "trusted project" entry.
Migration: if trusted-project-macros exists, the audit command warns that it's deprecated and suggests adding explicit requires: fields to the relevant ways.
Consequences¶
Positive¶
- Discoverable —
requires:is visible in frontmatter, not buried in a separate file - Granular — per-way and per-sensor permission declarations
- Auditable — one command shows the full permission gap
- Consistent — same model for ways and attend, using
agent-fmtTable output - Single authority —
settings.jsonis the only place permissions are granted - Self-documenting — reading a way's frontmatter tells you what it needs
Negative¶
- Existing ways need
requires:fields added (can be done incrementally) - Built-in sensor requirements are hardcoded (but rarely change)
Neutral¶
- Ways without
requires:are assumed to need no special permissions (static-only ways) - The audit is advisory — it doesn't block execution, it reports gaps
frontmatter-schema.yamlgains one new field
Declarative Config for Ways¶
Ways currently scatters configuration across dynamic shell checks (matcher selection, default language, corpus settings). Following attend's lead (ADR-115), ways should read from a config file at $XDG_CONFIG_HOME/ways/config.yaml with project overlay at $PROJECT/.claude/ways.yaml.
This config would hold:
- permissions: — the requires: audit settings
- matcher: — force semantic over BM25, or vice versa
- language: — default locale (e.g. en)
- corpus: — rebuild policy, staleness threshold
Dynamic checks in the code become config reads. When a config value is missing, the check says what to set rather than guessing — "have an opinion." This is the same pattern attend established and should be a shared convention across all agent-ways tools.
Full config schema design is out of scope for this ADR but should be addressed alongside or immediately after implementation.
Implementation Plan¶
- Add
requires:tofrontmatter-schema.yaml(optional field, string array) - Add
requires:parsing to attend sensor config - Implement
ways permissions auditin ways-cli (reads frontmatter + settings.json) - Implement
attend permissions audit(reads sensor config + settings.json) - Add
requires:to existing macro-bearing ways (github, adr, quality, etc.) - Hardcode built-in sensor requirements in attend
- Deprecation notice for
trusted-project-macrosin audit output - Update
docs/hooks-and-ways/macros.mdsecurity model section - Add
requiresvalidation toways lint— flag ways withmacro:but norequires:as warning - Implement
ways lint --fixauto-population — scan macro.sh for commands, generaterequires:field - Containment-aware permission matching (not string equality) in the audit engine