Appearance
Reviewing a pull request with Claude
A first pass over a pull request takes time, and most of it goes on reading lines that are fine. dqx review has Claude read only the changed lines, shows you what it found, and posts the findings you pick as comments on the pull request. Nothing reaches the pull request without your say-so.
bash
cd ~/projects/shop
dqx review # pick from the open pull requests
dqx review 222 # or name oneIt works on Bitbucket Cloud and GitHub.com. GitLab, GitHub Enterprise and a self-hosted Bitbucket are not supported; --branch still reviews locally there, without posting.
The same review has a tab in the dashboard, with the findings laid out like a diff: reviewing pull requests in the dashboard. Beside it sits the human review, for what only you can judge: the pull request in the order the change runs, your own comments on any line, and code changes drafted by Claude for the lines you pick.

Before the first review
| forge | what dqx needs | how |
|---|---|---|
| Bitbucket Cloud | an API token of yours | dqx auth --set bitbucket, below |
| GitHub.com | the gh CLI, logged in | gh auth login, below |
| both | Claude Code, logged in | claude on your PATH |
dqx doctor --all tells you where you stand, with one line per forge:
text
bitbucket ✓ Bitbucket token for you@example.com, 23 days left
github ✓ GitHub via gh (user you)Neither line ever fails the doctor: a machine that does not review pull requests is a fine machine.
A review, start to finish
text
acme/shop · 2 open pull requests
# title source → target
222 Course policy: seats and enrolment feature/x → development
224 WIP feature/y → development draft
Review which? 222
This sends code of acme/shop to Claude. Only for repositories where the CTO approved Claude Code.
Continue? (y/N) y
claude is reading app/Policies/CoursePolicy.php (1m 12s, ~$0.41 usage estimate)
#222 Course policy: seats and enrolment
feature/x @ 740161e7 → development @ 2e3f6d62
2 files, +40 -2 · reviewed 2026-09-24 18:06 UTC · model default · language en
https://bitbucket.org/acme/shop/pull-requests/222
10 findings:
1 ! app/Policies/CoursePolicy.php:62 Unenrolling after the start is silently ref…
2 ! app/Policies/CoursePolicy.php:66 Seat release is not atomic
3 ! …Http/Controllers/EnrolmentController.php:14 Seat input is not validated
4 · …Http/Controllers/EnrolmentController.php:14 The enrolment result is never used
5 · app/Models/Course.php:7 Start date cast drops the time
6 ! routes/web.php:14 Enrol route has no rate limit
7 ! resources/js/components/CourseTable.vue:127 Description rendered as raw HTML
8 · resources/js/components/CourseTable.vue:356 Page is concatenated into the URL
9 · resources/js/components/CourseTable.vue:425 Watcher loads on every keystroke
10 ? (general) No tests cover unenrolling
Post which? [e.g. 1,3-5 · essentials · all · none · o <n> · e <n> · s <n> <severity>] 1-3,6,7
https://bitbucket.org/acme/shop/pull-requests/222
Post 5 comments on #222? (y/N) y
posted 5 comments on #222 → https://bitbucket.org/acme/shop/pull-requests/222In that order:
- Which pull request. Open and draft ones, drafts labelled, each with what an earlier review left behind (
last review 740161e7 2 h ago, 5 of 10 posted). The target comes from the pull request, so a hotfix intomainis reviewed againstmainwithout anyone saying so. - Consent, once per repository. See why it asks.
- The run. dqx fetches the pull request, makes a throw-away worktree of its head, writes the pull request's own diff into it and starts Claude there. One progress line says what Claude is reading, how long it has been going and the usage estimate.
- What it was about. The header names the head and the merge base the diff was taken against, so you can see it is the three-dot diff: only what the branch changed, not what
developmentgained meanwhile. - The findings, numbered.
!needs attention (blocker, major),·could wait (minor, nit),?is a question for the author.(general)is a finding without a line in the diff. - Your pick, then a confirm that names the count and the pull request, then the post.
A project with several repositories
A project often has more than one repository: a wrapper with the Docker setup and a checkout per site. dqx review lists the open pull requests of all of them, grouped per repository, and you answer with the row number, because pull request numbers repeat across repositories:
text
3 repositories · 4 open pull requests
row # title source → target
docker · acme/qw-docker · none open
crm · acme/qw-crm · 2 open
1 #222 Course policy: seats and enrolment feature/x → development
2 #224 WIP feature/y → development draft
sites · acme/qw-sites · 2 open
3 #222 Course policy: seats and enrolment feature/x → development
4 #224 WIP feature/y → development draft
Review which? (row, or repo#nr) 3- Which repositories. With a
.dq/config.yml: every configured site's checkout plus the wrapper at the project root. Without one, dqx finds them itself: the project root and git checkouts up to two levels inside it, skippingvendor,node_modulesand hidden directories. Only checkouts on Bitbucket Cloud or GitHub.com count, and a second checkout of the same repository is shown once. - Short names. The prefix all repository names share is left off, so
acme/qw-crmshows ascrm, as long as that keeps every name unique. - Picking directly.
dqx review crm#222skips the list. A baredqx review 222works when only one repository has #222; otherwise dqx names the candidates and asks which. - When one repository cannot be listed (no token,
ghnot logged in), its group says why and the others still list. dqx does not guess past it: a bare number that might also be in the failed repository is refused with therepo#nrto type. --site <name>narrows the list to that one checkout.

A project without a dqx config can be reviewed too. Its settings come from your machine configuration and the profiles built into dqx; each repository's own .dq/review.md still applies.
Picking what gets posted
One line of input, the numbers from the table:
| you type | what it does |
|---|---|
1,3-5 | those findings (5-3 is read as 3-5) |
Enter, or essentials | blockers and majors |
blockers | blockers only |
all / none | everything / nothing |
o 3 | show finding 3 in full, with the lines of the diff it is about |
e 3 | edit its title and body in $VISUAL or $EDITOR, laid out like a commit message |
s 3 minor | change its severity: blocker, major, minor, nit or question |
o, e and s ask again afterwards; anything else is the selection. Edits are saved at once in the review file, and the table marks an edited finding (edited). Comments of your own are written in the dashboard's human review, and go out in the same post as the findings you pick there.
When the pull request moved, or was reviewed before
A review can take twenty minutes, and people push in the meantime. Before the table, and again before posting, dqx looks at the pull request as it is now:
- The head moved. Every finding is placed again by the text of its line, not its number: the nearest line with the same text in the new diff. A finding that cannot be placed that way, because its line is gone or appears twice at the same distance, is listed under moved and could not be placed - pick again to post as general comments.
allandessentialsskip it. Picked by number, it is posted as a general comment that names the file and the old line with the commit it was on, never inline on a guessed line. - It was already posted. Findings whose comment is already on the pull request are listed under already on the PR, unselected. dqx recognises its own comments by the footer, the file and the title, so this works for a colleague's run and for a review of an older commit too. A comment without the footer is a person's, and never counts.
Posting twice from the same review skips what went out the first time.
What lands on the pull request
Each finding becomes a comment that starts with its weight and title in bold, in the review language, and ends with one line:
text
**[blocker] Unenrolling after the start is refused without a reason**
The policy returns `false` once the first lesson has begun, ...
_(via dqx/claude)_- Bitbucket: one inline comment per finding on its line, and the general findings together in one general comment. Posted one after the other; if Bitbucket refuses one, dqx stops and says what went out and what did not. The review file is updated after every comment, so an interruption loses nothing.
- GitHub: one review with event
COMMENT, carrying the inline comments and the general findings in its body. One notification for the author. If GitHub refuses the batch (a line it will not take), dqx posts the comments one by one so the rest still lands.
dqx never approves, requests changes or declines. A verdict stays human.
Why every comment carries the footer
AI output is a draft until a person has read it, and that is Dolphiq's rule for anything a model writes. The footer tells whoever reads a single comment, in the pull request, an e-mail notification or the diff, that it came out of a model, and it is how dqx finds its own comments again. It cannot be edited or removed per comment. Claude's summary of the whole review is only posted when you tick it in the dashboard.
Why it asks before sending code
The diff and whatever files Claude opens go to Anthropic, through your own claude login. Client code only goes to an AI tool the CTO approved for that repository, so the first review of a repository asks:
text
This sends code of acme/shop to Claude. Only for repositories where the CTO approved Claude Code.
Continue? (y/N)The answer is remembered per repository in ~/.local/share/dqx/review-consent.json. --yes skips the question for a script but still records it. Without a terminal and without --yes, dqx refuses. If you are not sure the repository is approved, the answer is no, and the ISMS procedure is where to ask.
What Claude can and cannot do
| reads | the diff dqx wrote (.dqx-review.diff), and any file of the pull request's head (its Claude configuration excepted), with Read, Grep and Glob |
| cannot | edit or write files, run a shell, reach the network, start an MCP server, read outside the worktree, move a branch |
| works in | a detached worktree in your temp directory, removed afterwards (--keep leaves it) |
Suggest with AI in the human review is narrower still: Claude gets one file and the picked lines in its prompt, no tools at all, and an empty folder to start in. It can only answer with text, which lands in your comment editor as a draft.
No shell at all, not even git diff: git diff --output writes files and a textconv driver runs a program, so dqx does the git work itself and hands Claude the result. Your working copy, your branch and your databases stay as they were. Nothing is installed or run in the worktree: the review reads code, it does not execute it. Your own git hooks do not run either: dqx makes and fetches the worktree with hooks switched off, so a post-checkout hook never sees the pull request's code.
The worktree is someone else's code, and any comment in it can try to talk Claude into something. So Claude Code runs in restricted mode (--restricted), which ignores every settings file: yours in ~/.claude/, and the project's .claude/settings.json and .claude/settings.local.json. An allow rule you gave your own sessions, such as Bash(*), does not reach the review. The only tools that exist are Read, Grep and Glob (--tools), they stay inside the worktree, and no MCP server starts, neither yours nor one a .mcp.json declares (--strict-mcp-config with an empty list). Your Claude Code hooks are off for the run too.
The Claude configuration comes from the target branch. Before Claude starts, dqx puts back the target's version of every CLAUDE.md (any directory), file under .claude/ and .mcp.json the pull request changed, and deletes the ones it added. The project's own conventions still shape the review; a pull request cannot rewrite them to tell the reviewer to find nothing.
A pull request that changes the Claude configuration gets a finding of its own
Once merged, those files apply to every Claude session in the repository, so the change needs a human. dqx adds a finding, marked as coming from dqx and selected by default, whenever the diff touches CLAUDE.md, .claude/, .mcp.json or the review instructions file: check this by hand.
Settings
A review runs with a model, an effort (how hard Claude thinks), the languages it writes in and, when you want one, a profile. Each can be set in several places, and the last one wins:
- dqx itself, which ships three built-in profiles and nothing else
- the machine,
~/.config/dqx/config.yml - the project,
.dq/config.yml, committed - a branch rule of the project, which picks a profile for a pull request's first review
- the profile itself, with its own instructions, model and effort
- what this pull request's last review ran with
- this run's own flags, or the choices on the dashboard's pull requests page
yaml
# ~/.config/dqx/config.yml - your machine
review:
language: nl
codeLanguage: en
model: sonnet
effort: mediumyaml
# .dq/config.yml - the project, committed
review:
language: en
codeLanguage: match
instructionsFile: .dq/review.md
targets:
"hotfix/*": main
"*": development
profiles:
security: # replaces dqx's own security profile in this project
instructions: .dq/review-security.md
model: opus
effort: high
profileFor:
"hotfix/*": quick # dqx's own quick profilebash
dqx review 222 --language en --code-language match # this run only
dqx review 222 --model opus --effort high
dqx review 222 --profile quick # a built-in: works in any repository, config or not
dqx review 222 --profile none # the plain review, whatever a rule or the last run chose| key | what it does | default |
|---|---|---|
language | the language findings are written in: any ISO 639-1 code | nl |
codeLanguage | the language of code samples in findings: a code, or match to follow the identifiers in the codebase | en |
instructionsFile | the review instructions, relative to each repository's root | .dq/review.md |
targets | what --branch compares against, first match wins | hotfix/* → main, everything else → development |
model | the Claude model: an alias such as opus or sonnet, or a full model id | the CLI's default |
effort | how hard Claude thinks: low, medium, high, xhigh or max | the CLI's default |
profiles | named ways of reviewing, each with its own instructions, model and effort, all optional; a name dqx ships replaces that built-in | the built-ins: quick, thorough, security |
profileFor | source branch glob → profile, first match wins, for a pull request's first review | none |
budget | a cap in US dollars per review, for API-key users | none |
mapFlows | when the flow map is made: on-request from the human review or --map, or with-review by every review's own Claude run | on-request |
suggestProfile | the profile whose model and effort Suggest with AI runs with; only its model and effort count | quick |
A key with a bad value is dropped with a warning and the rest still apply; so is one bad profile or branch rule. A bad --effort or an unknown --profile is refused before anything is fetched. targets and profileFor are replaced whole by a later layer, because their rules match in the order written. profiles merge by name, from dqx's own through the machine to the project: a machine-wide migrations profile stays available in a project that only adds security, and a profile of the same name replaces the earlier one whole. The name none is reserved.
A different language after a review means a new run: dqx does not translate a finished one.
Profiles
A profile is another way of reviewing, not a tweak. Its instructions file replaces the project's .dq/review.md for that run, because a security review that still carries every point of the everyday checklist is not a security review. A profile without instructions keeps the project's file. Your machine's ~/.config/dqx/review.md still comes first. Like the project file, a profile's file is read from the target branch, so commit it there before the first review that uses it.
A branch rule (profileFor) matches the pull request's source branch and chooses the profile for its first review only. It can name a built-in profile. The pull request description never takes part: its author could lower their own review with it.
Built-in profiles
dqx ships three profiles, so dqx review --profile quick works in any repository, one without a dqx config included:
| profile | model | effort | its instructions |
|---|---|---|---|
quick | sonnet | low | only blockers and majors, each in a title and a sentence or two, and a two-sentence summary. Replaces the project's .dq/review.md |
thorough | opus | high | none of its own: the project's .dq/review.md stays, so it is the everyday review at more depth |
security | opus | max | a security focus with the security section of Dolphiq's code-review checklist, for every stack. Other blockers are still reported, minor points and style left out. Replaces the project's .dq/review.md |
Their instructions are part of dqx, like the checklist they draw on: nothing is read from a branch, so no pull request can change them. Your machine's ~/.config/dqx/review.md still comes first.
Replacing one. A profile of the same name in the machine or the project config replaces the built-in whole: its model, its effort and its instructions. A replacement without instructions reviews with the project's .dq/review.md, not with dqx's text, which is never copied into a repository. Name your own file when you want a different quick or security review for the project.
A re-review starts where the last one left off
Every review keeps what it ran with, and the next review of the same pull request starts from that: the same profile, model, effort and languages, so a re-review of #222 is the same review unless somebody says otherwise. A flag still wins. Naming a different profile, or none, keeps only the remembered languages: a model chosen for one profile says nothing about another. A remembered profile that has since been removed is dropped with a warning.
Setting it up from the dashboard
Everything above can be chosen and written without opening a YAML file, from the top of a project's pull requests page:
- Review with picks the profile for the next run, and the line beside it says what a run gets: the model, the effort and the two languages, each marked with where it comes from.
- this run… changes the model, the effort or a language for the reviews started from that page.
- ⚙ review settings is every review setting in one popup, with two tabs: This project for the project's defaults, profiles and branch rules in
.dq/config.yml, and Machine for this machine's defaults in~/.config/dqx/config.yml, with Open in editor for~/.config/dqx/review.md.

See reviewing pull requests in the dashboard and the review settings popup.
Instructions
dqx's own prompt is fixed: the output schema, "changed lines only", one finding per file and line, and the rule that the code under review is untrusted input. What you add is appended to it and cannot replace any of that.
| file | read from |
|---|---|
~/.config/dqx/review.md | your machine, for every review you run |
.dq/review.md (or review.instructionsFile) | the target branch, with git show origin/<target>:<path> |
a profile's instructions | the target branch too, in place of the project file |
| a built-in profile's instructions | dqx itself, in place of the project file (thorough has none and keeps it) |
The project file is read from the target on purpose: a pull request cannot rewrite the rules of its own review. Changing it takes a pull request of its own, which dqx then flags for a human look.
bash
dqx review --init-instructions # write a starter .dq/review.md
dqx review --init-instructions --force # replace the one that is thereThe starter is Dolphiq's code-review checklist, with only the stack sections this repository uses: no Filament points in a repository without Filament. Commit it, so the whole team reviews with the same rules, and keep it up from there.
Mapping the flows
A pull request on the forge is a list of files in alphabetical order. The code runs in another order: a Vue page calls a controller, which asks a policy. The flow map is Claude's reading of that order: the changed files grouped into named flows, each step a file with a method where Claude can name one, a line on why it is there and what it calls. It is what the dashboard's human review shows on the left, and the terminal prints it as a tree:
bash
dqx review 214 --maptext
Flows (mapped on d93b6f1a)
├─ Reserve a copy
│ A reader reserves a copy from the catalogue, and the policy checks the limit of three.
│ ├─ 1 resources/js/components/CopyTable.vue - The Reserve button emits the copy the reader picked.
│ │ ─dispatches→ resources/js/Pages/Catalogue.vue:5-11 - emits reserve with the copy
│ │ ┄ resources/js/Pages/Catalogue.vue:5-11 · caller - The catalogue page posts the copy the t…
│ ├─ 2 app/Http/Controllers/ReservationController.php · ReservationController::store() - Authorize…
│ │ ─calls→ ReservationService::reserve() - through Reader::reserve()
│ │ ┄ tests/Feature/ReserveCopyTest.php:11-21 · contract - The one test of reserving; it never…
│ ├─ 3 app/Policies/ReservationPolicy.php · ReservationPolicy::create() - Checks the reader's limi…
│ │ ─queries→ app/Models/Reader.php:11-20 - counts the active reservations
│ │ ─reads→ config/library.php:5-6 (unverified) - the limit of three
│ │ ┄ config/library.php:5-6 · data origin - The limit of three comes from here, where LIBRARY…
│ └─ 4 app/Services/ReservationService.php · ReservationService::reserve() - Reserves the copy, no…
│ ─writes→ app/Models/Reader.php:11-20 - adds a reservation
│ ─moved→ LoanService.php:27-37 - moved, 11 lines
│ ┄ app/Models/Reader.php:11-20 · schema - reservations() holds only active ones, and canRes…
├─ Overdue loans
│ A reader's overdue loans now come from the loan service, counted from the start of today.
│ ├─ 1 app/Services/ReservationService.php · ReservationService::overdueFor() - Gives up overdueFo…
│ └─ 2 app/Services/LoanService.php · LoanService::overdueFor() - Lists the reader's overdue loans…
└─ In no flow: CLAUDE.md
Layers
├─ Domain (3)
│ ├─ app/Policies/ReservationPolicy.php +17 −0
│ ├─ app/Services/LoanService.php +12 −0
│ └─ app/Services/ReservationService.php +7 −19
├─ Entry (1)
│ └─ app/Http/Controllers/ReservationController.php +14 −0
├─ UI (1)
│ └─ resources/js/components/CopyTable.vue +8 −1
└─ Other (1)
└─ CLAUDE.md +1 −0--map maps the newest finished review of the pull request and does not review it again; without one it reviews first and then maps. A map of the current head is printed as it is, without a new run. Under Layers every changed file is listed by what it is, from its path alone: Foundation, Domain, Entry, UI, Tests and Other. A file the map left out is always there, so the tree never hides part of the pull request. --map needs a pull request: a --branch review has none to map.
Arrows. The same run also names how the files relate, which the dashboard draws as the change map. Under each step, the tree prints its arrows: ─calls→ ReservationService::reserve(), with the kind first (calls, dispatches, reads, writes, queries, moved and a few more) and Claude's label after the dash. dqx checks every arrow with git grep at the pull request's head, on your machine: when the name it points at is not in the file it starts from, the arrow is marked (unverified). That means dqx could not confirm it, not that it is wrong. A line with ┄ is context: unchanged code the change depends on, such as config/library.php:5-6, where the limit of three comes from. A step in two flows lists its arrows once, where it first appears. A map made before arrows existed prints its steps as before. The tree is plain text, so it reads the same in a pipe or with NO_COLOR.
Mapping is one Claude run with the isolation of a review, on the same code, so the same consent covers it. The map is kept in the review file for the head it was made on; when the pull request moves, the human review offers to map it again.
With every review. In a project whose team always reads by flow, mapFlows: with-review has the review's own Claude run return the map as well: one run, with the context it already read, and the map is there when the review finishes. It costs a little more per review. Set it in .dq/config.yml or in the dashboard's review settings:
yaml
# .dq/config.yml
review:
mapFlows: with-review # default: on-requestFixing the feedback
After a review, the same points always need the same work: read each one, find the code, decide, fix and test again. dqx review fix hands that to Claude Code, with you steering:
bash
dqx review fix 214 # the open points, ticked as listed
dqx review fix 214 --points 1,3-4 # only these, by their number in the list
dqx review fix 214 --print # print the brief and the command instead of opening TerminalIt lists the points, numbered, with a tick for each one it takes:
- the AI findings of the pull request's last review (every finding counts as kept);
- your own comments from the human review;
- your colleagues' comments on the pull request. Open threads are ticked and resolved ones are not, as for a finding or comment of yours whose thread was resolved. A forge that can't say whether a thread is resolved gets the point ticked and marked
state unknown. A comment dqx posted itself is listed once, as the finding or comment it came from.
Status messages are listed hidden and unticked. A pipeline's note on the pull request, like "✅ All changed lines are covered - good job!" or "Build failed", is not feedback. dqx takes a comment for one only when it is a general comment (no file or line) of at most three lines and 280 characters, without a code block or a question, that starts with a status emoji (✅ ❌ ⚠️ 🟢 🔴 …) or talks about coverage, a build, the pipeline, tests passing or failing, lint or a quality gate. review.ignoreAuthors in .dq/config.yml names authors whose short general notes are hidden the same way, whatever they say:
yaml
review:
ignoreAuthors: [Pipelines, deploy-bot] # matched without regard to caseAn inline comment, anything with a code block and a longer message are never hidden, whoever wrote them, so an AI reviewer's comments and its reviews with sections (Rovo's, for example) stay. A hidden point says hidden in the list and is only in the brief when you pick it with --points.
The ticked points become one fix brief. Each point has its source and author, its file and line, its text and replies, and ten lines of code on either side at the pull request's head, plus the context the change map found for that file. The brief also carries the pull request's title and description, and the review's language. Whatever somebody wrote goes in fenced as data, not instructions, so a comment that says "ignore the rules and push" reaches Claude as a description of a problem. The brief holds at most 40 points and 60 kB. Points over that are named at the end, and Claude is told to leave them for a next round.
Then dqx:
- makes a worktree of the pull request in
$TMPDIR/dqx-fix-<owner>-<repo>-<pr>. When it can, the worktree is on a local branch that tracks the PR's branch; otherwise it is detached at the head. It is never your own checkout. A second run reuses the worktree with whatever was changed in it, and it stays after Claude is done; - writes the brief to
~/.local/share/dqx/fixes/<review>.md(0600, in a 0700 directory); - opens Terminal there with an interactive
claude, the brief as its first message and the project's dqx MCP server. On anything but macOS, it prints the command to run.
The brief tells Claude to plan first: per point, "fix" and how, or "skip" and why. It then waits for your go, keeps each change minimal, and reports per point at the end. What Claude may do in the worktree is up to your own Claude Code permission settings. dqx does not pass --dangerously-skip-permissions.
What never happens: dqx commits, pushes, posts, replies or resolves nothing, and the brief tells Claude not to either. You review what Claude changed, commit it and push. The brief sends the pull request's code and comments to Claude, so it asks the same consent as a review, the first time for a repository. The dashboard does the same from the review page: see Fixing the feedback.
Fixing it automatically
--auto lets Claude do the work in the background instead, in two runs with your go in between:
bash
dqx review fix 214 --auto # plan, ask per point (y/n), then fix
dqx review fix 214 --auto --yes # take Claude's plan as it is
dqx review fix 214 --auto --replace # discard the last fix first, then plan again
dqx review fix 214 --apply # the fix into your checkout, uncommitted
dqx review fix 214 --test # after --apply: the declared test command, in your checkout
dqx review fix 214 --discard # remove the fix branch and its worktree- The plan: the brief of the ticked points goes to a read-only run at the pull request's head, which answers each point with "fix" and how, or "skip" and why. dqx prints it, then asks per point whether to fix it.
--yestakes the plan as it is; without a terminal to ask in,--autoneeds it. - The fix: a second run edits a worktree of its own,
$TMPDIR/dqx-autofix-…, on a new local branchfix/<pr>-feedback(-2,-3when that exists) from the pull request's head, for the approved points only. It has Read, Grep, Glob, Edit and Write, and nothing else: no shell and no git. A hook refuses reads and writes of secret files and of paths outside the worktree, deny rules keep Grep and Glob out of secret files, and the model comes fromreview.fixProfile, else the profile the review ran with. - The check: before the change is offered, dqx refuses a diff that touches a secret file, a path outside the repository, a symlink, a binary file or a lockfile no point asked for, or that is over 200 kB, and one that changed any checkout of the project: the review's, the project root and each site's. The branch is kept to look at.
It then prints per point what was done, the diff stat and "tests not run in the fix", and the commands for what comes next. In the dashboard the fix opens as a review of its own, to read before applying it.
--applylooks again and refuses when your checkout is not on the pull request's branch, is at another commit than the fix was made at, or has uncommitted changes of its own in a file the fix touches. Otherwise it applies the change withgit apply --3wayand leaves it unstaged.--testruns the declared test command in your checkout, through the same path asdqx run:review.fixTest, else the command namedtest, else the first with "test" in its name. Its output is printed as it comes, then the exit code and passed or failed. It refuses when the project declares none.--discardremoves the worktree and thefix/<pr>-feedbackbranch, never another branch. A new plan is refused while the last fix is still there;--auto --replacediscards it the same way and plans again, in one job, and is refused while a fix run is running.
A pull request that moved since the plan refuses the fix: plan again. What never happens is the same as above: nothing is committed, pushed, posted or resolved, and the change reaches your branch only when you apply it.
A branch without a pull request
bash
dqx review --branch feature/seats
dqx review --branch feature/seats --target mainReviewed against --target, else the target of an open pull request from that branch, else the targets rules. The diff is git diff <target>...<branch>, made locally. Nothing is posted: there is nothing to post on. Works in any git checkout, on any forge.
In a script
bash
dqx review 222 --json --yes # the review as JSON, never a question
dqx review 222 --yes --post essentials # post blockers and majors, no questions--json prints { review, findings } and never prompts. --post takes the same selection as the prompt: none (the default), all, essentials, blockers or numbers. Without a terminal --post is the only way anything is posted, and it has to be said; on a terminal it still confirms unless --yes.
When a dashboard is running, the end of a run prints the link to the review's tab, and --open opens it.
Usage, time and cost
A review runs on your Claude plan and spends plan usage, not money: the plan's own limits apply. The progress line shows the CLI's cost estimate, labelled as one, as an indication of how big the review is. A trivial run already estimates around $0.30, because the system prompt, CLAUDE.md and the tool definitions come with every run.
A review is stopped after 20 minutes. That is the guard against a runaway review eating the usage window. If you use Claude Code with an API key rather than a plan, review.budget caps each review in dollars.
A review that fails, runs out of budget or answers something that does not fit the schema shows the reason and Claude's raw answer, and nothing from it can be posted.
Where a review is kept
Every run is a file in ~/.local/share/dqx/reviews/, named after the repository, the pull request and the head, for example acme-shop-222-740161e7.json. It holds the findings, your edits with Claude's original text, what was posted where, and the diff.
Those files hold snippets of client code, so they are readable only by you (0600, in a 0700 directory), and a review older than 30 days is removed on the next dqx review.
One review per repository at a time, whether it runs in a terminal or the dashboard. A second one is refused with who is running it and since when. Ctrl-C cancels: Claude is stopped, the worktree removed and the lock released, and the file is marked interrupted. A second Ctrl-C exits at once; a lock left by a process that is gone is taken over by the next run.
A Bitbucket token
bash
dqx auth --set bitbuckettext
Set up a Bitbucket API token for dqx review.
On the Atlassian page (opening it in your browser):
https://id.atlassian.com/manage-profile/security/api-tokens
1. Click "Create API token with scopes"
2. Name: dqx-yourmac
3. Expires: 2026-10-24
4. App: Bitbucket
5. Scopes - tick exactly these four, nothing broader:
[ ] read:user:bitbucket
[ ] read:repository:bitbucket
[ ] read:pullrequest:bitbucket
[ ] write:pullrequest:bitbucket
6. Create the token, then copy it
Atlassian account e-mail: [you@example.com]
Expiry date you picked: [2026-10-24]
Copy the token, then press Enter - read it from the clipboard? [Y/n]
read the clipboard and cleared it
checking the token with Bitbucket...
✓ Bitbucket knows it as You
✓ it can read the pull requests of acme/shop
stored the Bitbucket token for you@example.com in the login Keychain
✓ Bitbucket token for you@example.com, 30 days left
Rotate it on https://id.atlassian.com/manage-profile/security/api-tokens; `dqx auth --forget bitbucket` removes it here.Atlassian does not let a link pre-fill the token form, so dqx does everything around the one screen it cannot fill in: it opens the page, prints the name, the expiry date and the exact scopes, then reads the token from the clipboard, clears the clipboard, and checks it against Bitbucket before storing anything. Wrong e-mail or token: Bitbucket says 401 and nothing is stored. A missing scope: it says which to tick. Without a clipboard tool, or when you answer n, it asks for the token with a hidden prompt.
Why a token, when git already works over SSH
Your SSH key covers git: fetching and pushing. Listing pull requests and posting comments is Bitbucket's REST API, which only accepts HTTP authentication, and an Atlassian API token can only be made in the browser. Comments posted with it carry your name, which is what a review is.
The four scopes read your user, the repository and its pull requests, and write pull request comments. Nothing broader.

It expires. The default is 30 days from today and dqx accepts at most 90: a token that lives forever is a token nobody rotates. From five days before, dqx doctor --all, dqx auth and a chip in the dashboard's header say so; an expired or revoked token gets "run dqx auth --set bitbucket" wherever it is used. Rotate it the way the ISMS credential policy says, and keep nothing about it anywhere else.
Where it lives. On macOS the login Keychain, service dqx-bitbucket; on Linux the secret service through libsecret's secret-tool. The token goes to either on stdin, never as a command argument, so it is never on a process list, and dqx never prints it. The e-mail, dates and scopes, which are not secret, are in ~/.local/share/dqx/bitbucket.json, so the doctor and the dashboard can show the expiry without touching the token.
No Keychain and no libsecret, on a bare Linux box for example: dqx refuses, rather than quietly writing a file.
bash
dqx auth --set bitbucket --insecure-store # a 0600 file in ~/.config/dqx/credentials/
dqx auth --check # what is stored, never the value
dqx auth --forget bitbucket # remove it from this machine--insecure-store is a terminal flag and nothing else, so a plaintext token is always a conscious choice. The dashboard never offers it. --forget removes the token here; revoke it on the Atlassian page too, so it stops working everywhere.
A Keychain prompt after an update
After dqx self-update the binary is a new file, and macOS may ask whether it may read the dqx-bitbucket item. That is expected. Choose Always Allow.
GitHub, through gh
dqx keeps no GitHub token. It reads and posts through the gh CLI with your own login:
bash
gh auth login # once, with the repo scope
gh auth refresh --scopes repo # when an older login lacks itNothing to rotate in dqx and nothing in the Keychain: a missing or logged-out gh is answered with the command to run.

For whoever reviews dqx itself
- The token is never an argument, never printed, never in the dashboard's state, a review file, an error or the
-vvcommand echo. Tests check every output path for a sentinel value. - Client code leaves the machine only after the consent above, through the user's own login.
- Claude's tools are
Read,GrepandGlob, in restricted mode (--restricted --tools), with--permission-mode dontAskand no MCP servers. No shell, no edit, no network, no settings file of yours or the project's. - Posting is always shown and confirmed in a terminal; unattended only with an explicit
--post. - The dashboard's token, review, cancel, edit and post routes are refused by
--read-only, so a read-only dashboard can browse pull requests and reviews but not spend usage or post. - The code under review is untrusted input. It can only change the text of findings, which a person selects before anything is posted. The project's instructions and its Claude configuration (
CLAUDE.md,.claude/,.mcp.json) come from the target branch, and a change to them is flagged for a human. - Your git hooks do not run on the pull request's code: the worktree is made without them.
More in what dqx stores, and where.