Coduck logo Coduck

Documentation

Everything coduck does today, in the order you'll actually need it.

Overview

Coduck reviews what your coding agent is about to do, before it does it.

Status. Version 0.1.0. macOS and Linux, Intel and ARM.

Install

Coduck drives the coding agent you already run. It does not ship a model and it does not talk to a service of ours.

  • claude or codex on your PATH. Whichever you use, logged in as normal. Coduck calls it the way you would.
  • git, and a repository with a branch to review.

Nothing else. There is no account to create and no key to paste.

cd <the folder you unzipped this into>
sh ./install.sh

It picks the archive for your machine, checks it against checksums.txt, and refuses to install if they disagree.

It installs into the first conventional location that is both writable and already on your PATH, /usr/local/bin, Homebrew's bin, ~/.local/bin or ~/bin. If none of them qualifies it falls back to ~/.local/bin and prints the exact export PATH=... line to add, for the shell you run.

To choose the location yourself:

CODUCK_INSTALL_DIR=~/bin sh ./install.sh

Check it landed:

coduck --version      # 0.1.0

Configure a repository

From the root of the repository you want coduck to watch:

coduck init

It asks two things, which coding agent you run, and what kind of work lives here, then offers to point reviews at documents you already keep. Everything has a flag, so a scripted run skips the prompts.

coduck init --agent claude --domain backend --instructions AGENTS.md
FlagWhat it does
--rootRepository root (default: the enclosing git repository)
--agentCoding agent you run: claude or codex
--domainKind of work: backend, frontend, infrastructure, data-science. One per path, in order
--instructionsA document reviews should be judged against. Repeatable
--disable-gateTurn a gate off. Repeatable: plan, dependency, completion
--forceReplace existing settings

Name paths to answer separately for each, which is what a monorepo wants:

coduck init ./api ./web

Review

Three jobs, two commands.

coduck review --base main                              # your own work
coduck review --target origin/theirs --base main       # a colleague's branch
coduck review-pull-request --url <github pull request url>

Everything is a flag. Nothing is positional, so no argument depends on where it sits and none means two different things in two commands.

coduck review does two of the three

What --target does decides which:

ReviewedUncommitted workWhere
without --targetyour working treeincludedin place
with --targetthat refexcluded, a ref has nonea temporary clone

Cloning is not a preference. Reviewing a ref needs a working tree at that ref, and making yours hold it means a checkout, and git carries uncommitted changes across a checkout whenever they do not conflict, which quietly reviews your scratch file as part of somebody else's change. Nothing is downloaded, the objects are already in your .git and git hardlinks them.

Your branch and your own work are never touched, so reviewing a colleague's branch works in the middle of your own.

--base is required

It used to be guessed, origin/HEAD, then main, then master, and on a branch cut from another branch that reached back past everything already merged there, a ten-file change was reviewed as 176 files. Git does not record which branch a branch was cut from, so naming it is the only way to be sure.

Pull requests take a URL, not a number

coduck review-pull-request --url https://github.com/owner/repository/pull/1245

A bare 1245 is a pull request in some repository, and coduck would resolve it against whichever one you happen to be standing in. In an organisation with a frontend and a backend, that reviews a real pull request that is not the one you meant, and the report looks exactly like a correct one. The URL names the repository, so a mismatch is refused, and if you pass a number anyway, coduck builds the address from your own origin so it is one paste.

The base comes from the pull request itself, read straight from git, GitHub publishes pull requests as refs, so this needs no GitHub login, no token and no gh.

Open pull requests only. GitHub publishes the ref coduck reads the base from only while one is open. Review a merged one with coduck review --target <ref> --base <base>.
This spends money and takes minutes. A review is four reviewers, product, architecture, security and standards, each a separate headless call to the coding agent you declared at init, running read-only against your own logged-in session. Use --no-agents for the static checks alone, which takes about a second and costs nothing.
The exit code is always 0 for a verdict. It is not a merge gate.

When coduck refuses to review

Some comparisons cannot tell you anything however well the review runs, and coduck declines them before spending a minute and real money. There is one today, the base already contains every commit on your branch, and has moved on past it. Nothing of the branch can be in the diff, so a review of it says nothing about the branch, which is how a finished ticket once got reported as "entirely unimplemented".

Reviewing work you have not committed yet is not this case and is never refused. Pass --review-anyway to review regardless.

A refusal exits non-zero, unlike a verdict. It looks like this:

coduck review

NOT REVIEWED   the base already contains this branch

  this branch   feature/ENG-TASKS-1245-COPY   no commits of its own
  the base      origin/feature/launchpad      59 commits ahead

  Only uncommitted work could differ, so reviewing against this base
  cannot tell you anything about the branch.

  Review against the base this branch was cut from
      git log --oneline --graph --decorate -20

  Or review the uncommitted work on its own
      coduck review --base origin/feature/launchpad --review-anyway

Everything coduck says when it does not review goes to stderr, stdout carries a review or nothing, so a script reading it gets a report or an empty stream, never a sentence where a table should be.

Telling the product reviewer what was asked for

Without a requirement to check the change against, the product reviewer is skipped rather than guessing the requirements from the code it is reviewing.

FlagWhat it takes
--ticketThe ticket, a page address coduck fetches, or an identifier such as ENG-123 that only labels the report
--requirementsThe page holding what was asked for, fetched as you

A ticket is fetched from the system that holds it, or it is not supplied. Pasting one in, or pointing at a file somebody exported, produced a copy that was stale the moment the ticket changed, and a review judged against a stale requirement is confidently wrong in the way that is hardest to notice.

Connect Notion once with coduck auth notion, then share the page with coduck from its ••• menu, under Connections.

Reviewing a pull request

The base comes from the pull request itself, read straight from git, GitHub publishes pull requests as refs, and refs/pull/N/merge's first parent is the base tip. So this needs no GitHub token, no login and no gh, only the access that cloned the repository.

Open pull requests only. GitHub publishes that merge ref only while a pull request is open and merges cleanly, and takes it away once it is merged or closed. Review a merged one with coduck review --target.
GitHub only. GitLab and Bitbucket publish pull requests differently.

You do not have to check anything out. Coduck clones the repository into a temporary directory, checks the pull request out there, reviews it, and removes it. Your branch and your uncommitted work are untouched, so this works in the middle of your own change.

Nothing is downloaded to do it, after the refs are fetched, the objects are already in your .git, and git clone --local hardlinks them. Measured on a real repository, 0.27s, with the object store shared rather than copied, only the working files are new. The clone and the two refs/coduck/ refs are removed afterwards whether the review succeeded or not.

Settings still come from your repository, not the clone, which domain this is, which agent you run, and the documents reviews are judged against. The clone supplies code and nothing else.

Other flags

FlagDefaultWhat it does
--reviewersall fourRun some reviewers only: --reviewers security,standards
--targetyour working treeThe ref to review, cloned
--baserequiredWhat the change is measured against
--no-agentsoffStatic checks only, no model calls, no spend
--review-anywayoffReview even when a pre-check refuses
--rootthe enclosing repositoryWhich repository to review
--budget-dollars2.50Most one reviewer's call may spend
--timeout5mHow long one reviewer's call may run
--concurrency4How many reviewers to review at once
--verboseoffTrace every step, on stderr

What init writes

In the repository, committed:

  • .coduck/config.yaml, the domain, the coding agent, the documents reviews are judged against, and gate toggles. No secrets.
  • .coduck/.gitignore, keeps the ephemeral and personal files out of git.

Yours, gitignored:

  • .coduck/config.local.yaml, overrides anything in the committed file. This is where you say you run codex while the team's file says claude, rather than committing your preference onto everybody.

Ephemeral, safe to delete:

  • .coduck/cache/, .coduck/decisions/ and .coduck/runs/.

Settings are read committed-file-first, then the local one, then flags. A layer that does not mention something does not override it, maps merge key by key and document lists are combined, so a local file can add one note without restating the team's.

Which coding agent you run

coduck init asks which agent you use, claude or codex, and records it:

agent:
  name: claude

Coduck will use this to invoke your agent when it needs one. Nothing invokes an agent yet, the value is recorded so that work has something to read.

Hook registration only works for Claude Code. Declaring codex, cursor or copilot records the choice, skips hook registration, and says so in the summary, coduck's gates will not fire automatically for those agents yet. This is a separate question from which agent is detected on disk, a repository can carry a .claude directory from a colleague's setup while you run something else, so declaring your agent is what decides, not what happens to be lying around.

Pointing coduck at your own documents

Coduck decides how code is reviewed. Your team supplies what to judge it against.

This matters more than it looks, reviewing agents run restricted and load nothing on their own, not CLAUDE.md, not AGENTS.md, nothing in .claude/. Whatever your team has written down is invisible to a review unless it is named here.

# .coduck/config.yaml
instructions:
  all:          [AGENTS.md]
  architecture: [.claude/skills/architecture.md, docs/adr/*.md]
  security:     [.claude/skills/security.md]
  standards:    [.claude/skills/frontend-standards.md]

Everything under all goes to every reviewer, everything under a reviewer's name goes to that one. Globs are allowed and may match nothing, a team often lists docs/adr/*.md before writing the first one. A path named exactly and not present is an error, because somebody wrote it believing it was being read.

Two limits worth knowing. Paths must stay inside the repository. And each reviewer is a separate model call, so the same document is charged once per reviewer, there is a size limit, and going over it names the files rather than silently truncating your standards.

You cannot switch a reviewer off permanently or replace how coduck reviews one. --reviewers narrows a single run.

Gates and hooks

Three gates, registered against four Claude Code events:

GateEvent
planUserPromptSubmit
dependencyPreToolUse, scoped to Bash
completionStop and SubagentStop

coduck init merges these into .claude/settings.local.json, leaving your own settings and hooks alone. If no agent is detected it prints the mapping so you can wire it by hand.

That is Claude Code's personal settings file, not the shared settings.json. The hook command is the bare word coduck, resolved from your PATH, so a path that is right on your machine cannot break a teammate's checkout or CI.

coduck init adds .claude/settings.local.json to your .gitignore for you, unless a rule already covers it. It never creates a .gitignore, if the repository has none, init says so and leaves the file for you to make. Committing the settings file would hand every teammate a hook that runs whatever their own PATH resolves coduck to, which for most of them is nothing.

If the gates never seem to fire, check that coduck is on the PATH of whatever launches your agent. A GUI-launched editor does not inherit a shell's PATH on macOS, brew install and install.sh both place coduck in a directory that is already on PATH, but go install uses $(go env GOPATH)/bin, which is not.

Development

make test          # go test ./... -race
make lint          # go vet, gofmt check
make snapshot      # cross-compile all four platforms into dist/, publish nothing
make test-install  # exercise install.sh against dist/

Deferred

Homebrew and GitHub Releases are configured in .goreleaser.yaml but disabled. Enabling them needs a public repo, a homebrew-tap repo, and a tag, the config itself is already written and snapshot-tested.

Once the repo is public:

go install github.com/royihaddad/coduck/hosted/cmd/coduck@latest