Skip to main content
Built-in tasks (build, test, lint, format, gazelle, delivery) emit two kinds of follow-up suggestions when something noteworthy happens:
  • Repro commands“how do I reproduce this failure?” Rendered with a 🔁 Reproduce: header.
  • Fix commands“how do I apply the suggested fix?” Rendered with a 🛠️ Fix: header.
The same suggestions appear everywhere a task’s outcome is rendered: the CLI terminal output, Buildkite annotations, GitHub Status Check bodies, and the aggregate PR task summary comment. TaskLifecycleTrait.repro_fix_suggestion lets your .aspect/config.axl accept, reject, or rewrite each suggestion before any surface renders it. Common uses:
  • Rewrite suggested aspect … commands to your team’s preferred form — for example bazel … via the tools/bazel wrapper, or your repo’s own custom developer CLI (e.g. mycorp build … instead of aspect build …) when you’ve wrapped Bazel/Aspect behind a company-branded entry point.
  • Drop the vanilla-bazel alternate suggestion that built-ins tack on by default — useful when your team always has the Aspect CLI on PATH.
  • Add a description to a suggestion that ships without one, so the rendered line carries a # <label> above the command explaining what it does.
Requires Aspect CLI v2026.23.18 or newer.

Hook signature

.aspect/config.axl
load(
    "@aspect//traits.axl",
    "REPRO_FIX_ACCEPT",
    "REPRO_FIX_REJECT",
    "ReproFixInfo",
    "ReproFixSuggestion",
    "TaskLifecycleTrait",
    "repro_fix_replace",
)

def _my_hook(ctx: TaskContext, info: ReproFixInfo) -> ReproFixSuggestion:
    # ...inspect info, return a verdict...
    return REPRO_FIX_ACCEPT

def config(ctx: ConfigContext):
    lifecycle = ctx.traits[TaskLifecycleTrait]
    lifecycle.repro_fix_suggestion.append(_my_hook)
Hooks are invoked in registration order on every repro/fix entry, on every surface emit. Each entry runs through the chain at most once (idempotence is tracked internally), so it’s safe to register hooks that intermix with other features that also touch repro/fix data.

Verdicts

Return one of three values from your hook:
VerdictEffect
REPRO_FIX_ACCEPTKeep the suggestion unchanged. Chain continues to the next hook.
REPRO_FIX_REJECTDrop the suggestion entirely. Short-circuits the chain — no further hook sees it.
repro_fix_replace(command=, description=)Rewrite the command and/or description. Omit either field to keep the prior value. Chain continues with the rewritten entry.
A replace with both command and description left as None is a no-op (equivalent to accept).

ReproFixInfo fields

The single info argument carries everything a hook typically needs to decide:
FieldTypeNotes
task_pathstrJoined <group...> <name> CLI path, e.g. "lint", "dev test-repro-commands".
task_namestrJust the task name (e.g. "lint", "buildifier").
task_grouplist[str]The group prefix (e.g. [], ["dev"]).
kindstrTask-result kind — "lint_results", "bazel_results", "format_results", "gazelle_results", "delivery_results". Matches TaskUpdate.kind so hooks can route by the same key the surface templates use.
command_kindReproFixCommandKind"repro" for entries from 🔁 Reproduce, "fix" for entries from 🛠️ Fix.
commandstrThe suggestion’s shell-ready command string.
descriptionstrOptional short label rendered as a # <description> comment above the command. Empty when the task emits a single suggestion of its kind.
slugstrStable kebab-case identifier — the recommended scoping key. See suggestion slug catalogue below.
statusstrLifecycle status the task is about to emit: "running", "failing", "passed", "warning", "failed", "aborted".
exit_codeintTask exit code. Only meaningful on the terminal emit; 0 mid-task.
bazel_subcommandstr"build", "test", or "run" for Bazel-wrapping tasks; "" otherwise.
targetslist[str]Resolved target patterns the task ran on.
failed_targetslist[str]Deduplicated list of Bazel labels that failed. Empty on non-Bazel tasks.
extrasdictOpen-ended, task-stamped, curated keys. Documented per-task. Examples: lint stamps {"aspects": [...], "suggestion_count": N}; format and gazelle stamp {"affected_files": [...]}; delivery stamps {"mode": str, "targets_total": N}.
datadictEscape hatch — full task data dict for hooks that need fields not surfaced by the typed fields or extras. Treat as unstable; prefer the typed surfaces above.

Suggestion slug catalogue

Every suggestion carries a stable kebab-case slug so hooks scope by suggestion identity instead of parsing the command string. The full catalogue (as of v2026.23.18):
TaskSlugs
lintlint-repro, lint-fix
formatformat-repro, format-fix, format-fix-truncated, format-fix-rediscover, format-fix-vanilla-bazel
gazellegazelle-repro, gazelle-fix, gazelle-fix-truncated, gazelle-fix-rediscover, gazelle-fix-vanilla-bazel
deliverydelivery-repro-local-preview
bazel (build / test)bazel-repro-aspect, bazel-repro-vanilla-bazel
Convention: every vanilla-bazel alternate suggestion ends in -vanilla-bazel, so a single hook can suffix-match to scope across all producers without listing each slug.

Examples

Three production-grade examples, all currently shipping in the aspect-build/aspect-cli repo’s own .aspect/config.axl.

Rewrite aspect … to bazel … for one task

Useful when your repo has a tools/bazel wrapper and you want suggestions to use the bazel form so new contributors copy-paste a command that goes through the checked-in Bazel version.
.aspect/config.axl
def _buildifier_repro_fix(ctx: TaskContext, info: ReproFixInfo) -> ReproFixSuggestion:
    """Rewrite buildifier `aspect ...` suggestions to `bazel ...`."""
    if info.task_name != "buildifier":
        return REPRO_FIX_ACCEPT
    return repro_fix_replace(
        command = info.command.replace("aspect buildifier", "bazel buildifier", 1),
    )

def config(ctx: ConfigContext):
    lifecycle = ctx.traits[TaskLifecycleTrait]
    lifecycle.repro_fix_suggestion.append(_buildifier_repro_fix)

Drop the vanilla-bazel alternate everywhere except one task

Built-in tasks tack on a vanilla bazel run <target> suggestion next to their aspect … repros/fixes — a fallback for environments where the Aspect CLI isn’t on PATH. If your team always has aspect (or a tools/bazel wrapper that routes through it), those alternates are noise.
.aspect/config.axl
def _drop_vanilla_bazel_except_build(ctx: TaskContext, info: ReproFixInfo) -> ReproFixSuggestion:
    """Drop every vanilla-bazel alternate except `build`'s."""
    if not info.slug.endswith("-vanilla-bazel") or info.task_name == "build":
        return REPRO_FIX_ACCEPT
    return REPRO_FIX_REJECT

def config(ctx: ConfigContext):
    lifecycle = ctx.traits[TaskLifecycleTrait]
    lifecycle.repro_fix_suggestion.append(_drop_vanilla_bazel_except_build)
The endswith("-vanilla-bazel") suffix match catches every current producer (format, gazelle, build, test) and any new ones added later.

Rewrite a description without touching the command

repro_fix_replace takes either field independently — passing only description keeps the existing command unchanged.
.aspect/config.axl
def _shorten_delivery_local_preview(ctx: TaskContext, info: ReproFixInfo) -> ReproFixSuggestion:
    """Shorten the delivery local-preview description for consistency across runs."""
    if info.slug != "delivery-repro-local-preview":
        return REPRO_FIX_ACCEPT
    return repro_fix_replace(
        description = "--mode=always --track-state=false for off-runner with no state backend.",
    )

def config(ctx: ConfigContext):
    lifecycle = ctx.traits[TaskLifecycleTrait]
    lifecycle.repro_fix_suggestion.append(_shorten_delivery_local_preview)

Order matters

Hooks run in registration order, and each replace verdict feeds the next hook in the chain. So if you register a rewrite hook and a reject hook, register the rewrite first — otherwise the reject may match on the original slug and short-circuit before the rewrite runs (or worse, on a rewritten slug the next hook can no longer recognize). The slug is preserved across replace verdicts (hooks rewrite commands and descriptions, not suggestion identity), so slug-scoped hooks downstream of a replace still match correctly. But the rewritten command / description is what downstream hooks see.

How it fits into the lifecycle

apply_repro_fix_hooks runs automatically on every surface emit via the framework’s dispatch_task_update — no surface (CLI printer, Buildkite annotation, GitHub Status Check body, PR-comment rollup) ever renders an un-hooked entry. An idempotence flag on each ReproFixCommand makes repeated dispatches safe; each entry runs through the chain at most once even though every emit dispatches. Hooks see the same suggestions every consumer sees, in the same order, so a rewrite or rejection flows through to every surface in lock-step.

See also