Macros¶
Dynamic content generation for ways.
What Macros Do¶
A way's static content ({name}.md) is the same every time it fires. Macros add dynamic content by running a shell script at trigger time. The script's stdout is combined with the static content based on the macro: frontmatter field.
macro: prepend # macro output appears before static content
macro: append # macro output appears after static content
The macro script lives alongside the way file as macro.sh in the same directory.
What a Macro Receives¶
A macro gets no stdin and no arguments. The ways binary runs it with bash and exports what it may need about the session:
| Variable | Value |
|---|---|
CLAUDE_SESSION_ID |
The session the way fires for. Inside a subagent this is the parent's id, since subagent hooks report it |
CLAUDE_PROJECT_DIR |
The resolved project directory, never empty |
WAYS_SESSIONS_ROOT |
The per-user root of session state ({root}/{session}/…) |
WAYS_SCOPE |
agent, teammate or subagent: who the way is rendered for. subagent means injected at SubagentStart; a way a subagent's own tool hooks fire reads agent, with CLAUDE_AGENT_ID set to the subagent's id. A macro that consumes session state skips subagent, or it would take the parent's |
WAYS_CONTEXT_USED, WAYS_CONTEXT_REMAINING, WAYS_CONTEXT_PCT_REMAINING |
The context budget, read from the session's transcript |
WAYS_ENABLED_PLUGINS |
Enabled plugin ids (name@marketplace), one per line, from the enabledPlugins maps in Claude Code's settings files |
WAYS_ADR_TOOL, WAYS_DOC_TOOL |
The project-relative path of the first executable docs/scripts/, scripts/ or tools/ adr (doc), when there is one |
The last three rows cost a transcript read, a settings read or a few stats, so the binary computes each only for a macro whose source names the variable. A value is set only when the macro's own source names it: every other name in those rows is removed from the macro's environment, so a value exported by the shell that ran the hook never leaks in. A named variable with no value is unset too. A postcheck.sh gets CLAUDE_SESSION_ID, CLAUDE_PROJECT_DIR and WAYS_SESSIONS_ROOT, with the hook payload on stdin. A project-local postcheck runs under the same trust rule as a project-local macro (below).
Why Macros Exist¶
Some guidance depends on project state that can't be known at authoring time:
- Does this project use ADR tooling? (adr/macro.sh checks for
docs/scripts/adr) - Is this a solo project or a team project? (github/macro.sh checks contributor count)
- Which files in this project are too long? (quality/macro.sh scans the codebase)
Static way content handles the universal guidance ("here's how to write good commits"). Macros handle the situational guidance ("this project has 3 files over 800 lines - here they are").
Examples¶
ADR Tooling Detection (documentation/adr)¶
Implements tri-state detection:
- Declined:
.claude/no-adr-toolingexists. The macro outputs a one-liner noting the project opted out, then the v0 record format, and stops suggesting installation. - Installed:
docs/scripts/adrexists. The macro reads the vendored tool'sTOOL_VERSIONand thecontract:indocs/architecture/adr.yaml, and outputs the guidance for that combination (ADR-304 §10): the v0 commands and record format, the same plus a note that adopting adr/v1 is a decision, the adr/v1 guide, or a warning that the tool cannot enforce the declared contract. - Available: Neither file exists. The macro suggests installing ADR tooling, with setup instructions, and outputs the v0 record format.
The way body stays contract-neutral; everything that depends on the contract comes from the macro. tests/adr-macro-test.sh covers each state.
Team Detection (softwaredev/github)¶
Queries the GitHub API for contributor count: - Solo/pair project (1-2 contributors): relaxes PR requirements - Team project (3+ contributors): recommends PRs, shows potential reviewers
Adapts the formality of GitHub workflow guidance to the actual collaboration context.
File Length Scanner (softwaredev/quality)¶
Scans git ls-files for files exceeding length thresholds:
- Priority (>800 lines): files that likely need decomposition
- Review (>500 lines): files worth monitoring
Uses the scan_exclude: frontmatter field to skip files that are legitimately long (lock files, generated code, markdown).
The Core Macro¶
macro.sh at the ways root (~/.claude/hooks/ways/macro.sh) generates the Available Ways table shown at session start. It scans all way files, extracts their trigger patterns from frontmatter, and formats them as a markdown table grouped by domain.
This is invoked by ways show core during SessionStart and referenced by core.md via macro: prepend.
Security Model¶
Shipped and personal macros¶
Macros in the shipped ways (~/.claude/hooks/ways/) and in your personal root ($XDG_CONFIG_HOME/agent-ways/ways/) always execute. You installed the one and wrote the other, so trust is implicit.
Project-local macros¶
Macros in $PROJECT/.claude/ways/ are potentially untrusted (a cloned repo could include malicious scripts). These only execute if the project path is explicitly listed in ~/.claude/trusted-project-macros, one path per line.
A way's requires: field (ADR-116) is separate: it declares the tool permissions the way's macro needs, and ways author permissions compares those against the grants in settings.json. It does not make a project's macros trusted. ways author permissions reports trusted-project-macros as deprecated in favour of requires:, but the runtime still reads it, and it is the only switch that lets a project's macros run.
If a project-local macro exists but the project isn't trusted, the way outputs a note:
Note: Project-local macro skipped (add /path/to/project to ~/.claude/trusted-project-macros to enable)
This prevents supply-chain attacks through project-local way macros while allowing teams that trust their repos to use the full feature.