Skip to content

Customizing workflows and prompts

autoducks ships three supported customization surfaces: hooks, which run your own steps around each agent; prompt overrides, which extend or replace an agent’s instructions; and Claude settings, the base permissions/env/MCP configuration every agent run starts from. All three live outside the files autoducks manages, so they survive install.sh updates.

These three surfaces are also the primitives a plugin packages up: a plugin is the same hooks, prompt fragments, and settings described on this page, bundled with a manifest so they can be versioned and shared across repositories instead of hand-authored locally in one. Read this page first for how each surface works standalone; reach for Plugins when you want to distribute one.

Why you can’t edit the shipped files directly

Section titled “Why you can’t edit the shipped files directly”

The workflow files under .github/workflows/autoducks-*.yml are generated mirrors of the canonical templates in .autoducks/runtimes/github-actions/. scripts/setup.sh check 9 (“Runtime workflow sync”) runs diff -q between every template and its mirror and fails if they’ve drifted apart. The same applies to the agent prompts and scripts under .autoducks/agents/: an update replaces the whole .autoducks/ tree wholesale, so any hand edit inside it is silently discarded on the next update.

That’s why hooks and prompt overrides exist as separate, untouched directories instead of “just edit the mirror”: .github/actions/ and .autoducks/custom/ are never written by install.sh, so your customizations survive every update.

A hook is a composite GitHub Action that autoducks invokes immediately before and after each agent step. Place one at:

.github/actions/autoducks/<agent>-pre/action.yml
.github/actions/autoducks/<agent>-post/action.yml

install.sh never touches .github/actions/, so anything you put there is untouched by updates.

There are 24 hook points: one pre and one post hook for each of the 12 agent workflows.

AgentPre hookPost hook
architect.github/actions/autoducks/architect-pre.github/actions/autoducks/architect-post
engineer.github/actions/autoducks/engineer-pre.github/actions/autoducks/engineer-post
developer.github/actions/autoducks/developer-pre.github/actions/autoducks/developer-post
reviewer.github/actions/autoducks/reviewer-pre.github/actions/autoducks/reviewer-post
fix.github/actions/autoducks/fix-pre.github/actions/autoducks/fix-post
close.github/actions/autoducks/close-pre.github/actions/autoducks/close-post
revert.github/actions/autoducks/revert-pre.github/actions/autoducks/revert-post
maestro.github/actions/autoducks/maestro-pre.github/actions/autoducks/maestro-post
defer.github/actions/autoducks/defer-pre.github/actions/autoducks/defer-post
product.github/actions/autoducks/product-pre.github/actions/autoducks/product-post
resolver.github/actions/autoducks/resolver-pre.github/actions/autoducks/resolver-post
rework.github/actions/autoducks/rework-pre.github/actions/autoducks/rework-post

Example: installing Playwright before the Developer runs

Section titled “Example: installing Playwright before the Developer runs”
.github/actions/autoducks/developer-pre/action.yml
name: 'Install Playwright'
description: 'Set up a browser toolchain before the Developer runs'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Playwright
shell: bash
run: npx playwright install --with-deps chromium
- name: Log context
shell: bash
run: echo "Preparing developer run for issue $ISSUE_NUM on $REPO"

autoducks wires this in automatically — you don’t edit the workflow YAML yourself. The relevant steps in .github/workflows/autoducks-developer.yml look like this:

- name: User pre hook
if: >-
steps.authz.outcome == 'success' &&
steps.pre.outputs.duplicate_skip != 'true' &&
steps.pre.outputs.dor_skip != 'true' &&
hashFiles('.github/actions/autoducks/developer-pre/action.yml') != ''
uses: ./.github/actions/autoducks/developer-pre
env:
AUTODUCKS_AGENT: developer
AUTODUCKS_STAGE: pre
ISSUE_NUM: ${{ steps.ctx.outputs.issue_num }}
BASE_BRANCH: ${{ steps.ctx.outputs.base_branch }}
REPO: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id || '0' }}
RUN_ID: ${{ github.run_id }}
COMMENTER: ${{ steps.ctx.outputs.commenter }}
GH_TOKEN: ${{ secrets.AUTODUCKS_PAT || secrets.GITHUB_TOKEN }}
- name: Run LLM agent
id: llm
# ...
- name: User post hook
if: >-
always() && steps.authz.outcome == 'success' &&
steps.pre.outputs.duplicate_skip != 'true' &&
steps.pre.outputs.dor_skip != 'true' &&
hashFiles('.github/actions/autoducks/developer-post/action.yml') != ''
uses: ./.github/actions/autoducks/developer-post
env:
AUTODUCKS_AGENT: developer
AUTODUCKS_STAGE: post
ISSUE_NUM: ${{ steps.ctx.outputs.issue_num }}
BASE_BRANCH: ${{ steps.ctx.outputs.base_branch }}
REPO: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id || '0' }}
RUN_ID: ${{ github.run_id }}
COMMENTER: ${{ steps.ctx.outputs.commenter }}
AGENT_OUTCOME: ${{ steps.llm.outcome }}
GH_TOKEN: ${{ secrets.AUTODUCKS_PAT || secrets.GITHUB_TOKEN }}

Every hook receives a subset of these variables as environment variables:

VariablePresent onValue
AUTODUCKS_AGENTall 24 hooksthe agent name, e.g. developer
AUTODUCKS_STAGEall 24 hookspre or post
ISSUE_NUMall 24 hooksthe triggering issue/feature number
REPOall 24 hooksowner/repo
COMMENT_IDall 24 hooksthe triggering comment’s ID, or 0
RUN_IDall 24 hooksthe GitHub Actions run ID
COMMENTERall 24 hooksthe GitHub login that triggered the run
GH_TOKENall 24 hooksAUTODUCKS_PAT if set, else the default GITHUB_TOKEN
BASE_BRANCHdeveloper onlythe branch the Developer is building on
IS_PRreviewer, defer, resolver, reworktrue if the trigger was a PR event
AGENT_OUTCOMEpost hooks onlythe agent step’s outcome: success, failure, cancelled, or skipped

product uniquely adds COMMENT_ISSUE_NUM and DRY_RUN to its hook environment.

  • Absent hook ⇒ skipped. The hashFiles(...) != '' guard is empty when the action.yml doesn’t exist, so the step is a no-op — no error, no warning.
  • A failing pre hook blocks the agent. Neither the pre hook nor the agent step uses always(), so GitHub Actions’ default failure propagation stops the job before the agent runs. A broken pre hook means the agent never runs.
  • Post hooks run on always() (as long as authorization succeeded), so they fire whether the agent succeeded, failed, or was skipped — check AGENT_OUTCOME if your post hook needs to branch on that.
  • Developer and Engineer share their skip guards with the LLM step. Both agents can bail out early — a Definition-of-Ready delegation to the Maestro (dor_skip), or an idempotency guard when a PR already exists for the task (duplicate_skip, developer only). Their hooks reuse those same output checks, so a hook never fires on a run that’s really just a hand-off or a duplicate-dispatch no-op.
  • Maestro hooks are orchestrate-only. The maestro workflow has two jobs, resolve and orchestrate. Hooks only exist in orchestrateresolve doesn’t check out the repo, so a local composite action can’t be resolved there.
  • Product hooks are triage-only. The product workflow runs the LLM only in triage mode; in merge mode it runs merge.sh (bash, no LLM). The hooks share the LLM step’s mode == 'triage' guard, so they never fire on a /merge run.

Prompt overrides live under .autoducks/custom/, a directory install.sh preserves across updates:

.autoducks/custom/
├── instructions.md # appended to every LLM agent's prompt
└── agents/
└── <agent>/
├── instructions.md # appended to this agent's prompt only
└── prompt.md # replaces this agent's shipped prompt entirely

Only the 9 LLM-backed agents support prompt overrides: architect, engineer, developer, reviewer, fix, resolver, rework, defer, and product. Maestro, close, and revert are bash-only orchestration agents with no prompt to customize.

  • instructions.md (append) — added on top of the shipped prompt under a # Repository-specific instructions heading. This is the recommended path: your instructions layer on whatever the shipped prompt currently says, so you keep receiving upstream prompt improvements.
  • prompt.md (full replace) — used verbatim instead of the shipped prompt. The shipped prompt is never even read for that agent.

Both append files can be combined with a full replace: if agents/<agent>/prompt.md exists, it’s used as the base, and instructions.md (global, then per-agent) is still appended on top of it.

This resolution happens in .autoducks/core/config/resolve-prompt.sh, called once per LLM invocation by the claude provider action. With no .autoducks/custom/ tree at all, its output is byte-for-byte identical to the shipped prompt.

resolve-prompt.sh also appends any enabled plugin’s prompts/instructions.md and prompts/agents/<agent>/instructions.md, in declared plugins[] order, under a trailing # Plugin instructions heading — after your own overrides, so a repository’s own instructions always take precedence in reading order.

.autoducks/providers/llm/claude/settings.json, if present, is the base Claude Code settings object every agent run starts from — permissions (allow/deny), env, and mcpServers. install.sh never writes this file and preserves it across updates, the same as .autoducks/custom/. With no such file, the claude provider action runs with no settings override at all.

This is the surface a plugin’s claude/mcp.json, claude/hooks.json, and claude/settings.patch.json build on: apply-plugins.sh layers every targeting plugin’s contributions on top of this base file into a per-agent compiled settings file, without ever modifying the base file itself.

An install.sh update replaces the entire .autoducks/ tree, then restores three things from a stash: .autoducks/autoducks.json, .autoducks/providers/llm/claude/settings.json, and the whole .autoducks/custom/ tree. .github/actions/ is never touched by install.sh at all. In short:

PathOn update
.autoducks/ (agents, prompts, core scripts)Overwritten
.github/workflows/autoducks-*.ymlOverwritten (re-copied from the new .autoducks/runtimes/)
.autoducks/autoducks.jsonPreserved
.autoducks/providers/llm/claude/settings.jsonPreserved
.autoducks/custom/Preserved
.github/actions/ (your hooks)Preserved (never touched)
.autoducks/plugins/ and .autoducks/providers/llm/claude/compiled/Not preserved — re-run apply-plugins.sh after updating; see Plugins: update survival

See the Updating section of the installation guide for the update command itself.