Skip to content

AI tooling ​

dqx ships an MCP server, so an AI agent can ask about your environment instead of guessing at it.

bash
claude mcp add dqx --scope user -- dqx mcp

One registration covers every project on the machine.

What it answers ​

dqx knows where. Which projects exist, where a site's code lives on the host and inside the container, what to run and in which directory, where the database is, who holds 80 and 443, what is broken.

A framework's own tooling knows what. Your routes, your models, your config keys. Laravel Boost does this well and dqx does not reimplement it. The two sit beside each other, and dqx's tools are prefixed dqx_.

An agent without this guesses, and its guesses look plausible: it runs the test suite directly and reports a failure that is not real, because the project declares a prepare-test step it did not know about; or it reads a docker exec -w /var/www/… line from a stale guideline, lands in a directory that still exists but is half-empty, and concludes a command is missing. Each is a wrong answer produced confidently.

What it will not do ​

Read-only by default. Anything that changes what is running needs --allow-write, and some things are not exposed at any flag:

default--allow-writenever
reads - info, addresses, logs, doctor, verify, machine health, memory, scaffolds, the config schema✅
reading dqx reviews and their findings✅
running a command the project declared✅
running anything else✅
starting and stopping a project, the browser suite✅
snapshots (take, list, restore) and dqx sync✅
creating a new project✅
starting a review or posting its findings✅
installing, switching, cleaning or killing an existing project✅
repair, resolver install, loopback, auth, self-update, mcp register, the dashboard✅
a command that looks like a deploy or reaches a server✅ unless the project allows it

Without --allow-write the start and stop tools do not exist at all, rather than being refused: a tool an agent can see is a tool it will try. With the flag they are marked for the client to confirm, so Claude asks you before each one. The one place dqx itself asks is dqx_run on a project that is not running: it offers to start it, because nothing can run in a stopped project.

Which tool when ​

The server tells the agent this at the start of every session, so you do not have to:

tooluse itneeds --allow-write
dqx_project_list, dqx_project_infowhen it is unclear which project this is
dqx_list_commands, dqx_runto run what the project declares, in the right containerfor free-form commands
dqx_doctor, dqx_verifywhen a site does not answer: the project's findings, and what each domain answers
dqx_machine_status, dqx_machine_healthwhen the project looks healthy and still nothing answers
dqx_e2eafter a UI change, before calling it done✅
dqx_e2e_init, dqx_e2e_scenariono browser suite yet; a journey that needs people the seeders do not make (scenarios)✅
dqx_builda dev-server site's build, with the command its config names, while the dev server is down✅
dqx_review_list, dqx_review_findingsafter you ran a dqx review: the agent works through the findings
dqx_snapshot, dqx_snapshot_restorebefore a risky data change, and to go back✅
dqx_syncafter editing .dq/config.yml, with dryRun first✅
dqx_memorywhen a suite is slow or killed for memory

dqx_machine_health is dqx doctor --all, dqx repair --dry-run and dqx resolver status in one answer. It names each fix repair would make and whether that needs sudo, and runs none of them. It leaves out the Bitbucket and GitHub account lines, which name a person.

Reviews: the agent reads, you start and post ​

dqx_review_list shows the open pull requests of every repository in the project, each with its last dqx review. dqx_review_findings returns one review's findings: file, line, severity, the text, whether it went stale or is already on the PR, and the few lines of the diff around it. Never the whole diff, never Claude's raw output, never a token. A repository whose forge login is missing or expired says so in its hint.

Starting a review and posting it have no tool at any flag. A review costs plan usage under the consent you gave per repository, and a posted comment speaks for you on the PR. So the loop is: you run dqx review or review from the dashboard, the agent reads the findings and fixes what holds up in the code, and you decide what goes on the PR.

I ran a dqx review on #222. Use dqx_review_findings, fix what you agree with, and tell me which findings you think are wrong and why. Do not post anything.

Snapshots and sync ​

With --allow-write an agent can hang its own safety net. dqx_snapshot saves the databases and storage into .dq/snapshots/, labelled agent, and deletes no other snapshot, so an agent taking four in a row cannot push out yours. dqx_snapshot_restore needs a name from dqx_snapshot_list rather than taking the newest, and always takes a before-restore snapshot of what it overwrites first; there is no way to ask it not to. dqx_sync is dqx sync without --force or --prune: a file somebody edited by hand is reported and left alone.

Deploys stay with you ​

dqx never deploys, but a project can declare a script that does, and a declared command runs without any flag. So dqx_run refuses a command whose name or script looks like a deploy or a remote action, at any flag, the free-form shell behind --allow-write included:

  • the deploy family as a whole word: deploy, deploy-docs, deploy:production, redeploy, redeployment-test (hyphens, colons and underscores separate words, so deploy-docs counts)
  • a script named push, publish or release
  • reaching a server: ssh, scp, sftp, ftp, lftp, rsync
  • deploy tools: envoy, dep (Deployer), forge, vapor, ploi, cap, ansible
  • pushing and publishing: git push, docker push, npm publish (and yarn, pnpm, bun), semantic-release, release-it
  • cloud and cluster CLIs: aws, gcloud, gsutil, az, doctl, heroku, fly, vercel, netlify, firebase, wrangler, serverless, kubectl, helm, terraform, pulumi

It reads a composer or package script's body as well as its name, so a script called up whose body is an ssh is caught. It is a pattern, not a guarantee, and it errs towards refusing: a false alarm costs you one line, a miss costs a deploy nobody decided on.

When a command is safe for an agent in your project, allow it by name in .dq/config.yml. A free-form command is allowed by its exact text:

yaml
mcp:
  allowCommands:
    - deploy-docs
    - "git push origin docs"

The refusal says which word it matched and prints this block for the command it refused. The list is committed, so allowing an agent to run something is a change the team sees in review.

Creating is allowed and installing is not, which sounds inconsistent until you see the line: the tool refuses any directory that already holds a .dq/config.yml, so it can only ever create in a directory it just made.

Prompts that work ​

All three need --allow-write. Without it the agent says so.

A new application, from a recipe.

Create a new Laravel project called DQ Sample on dq-sample.dqx, with a database, redis and mailpit. Use dqx_scaffolds to pick the recipe, call dqx_init_project with dryRun first, show me what it resolved and what it assumed, and only then create it.

The dry run is what makes this safe: the tool answers with what it resolved and what it assumed. Read the assumptions. A PHP version from a recipe is fine; a domain invented from a client name may not be.

A project serving several applications.

Create a Qore-works Business project for client Zorgpad, layout: multi, with two sites: api.zorgpad.dqx from repository zorgpad-api and admin.zorgpad.dqx from zorgpad-admin. Ask dqx_config_schema for the keys before writing the from: document.

Several sites cannot be said in flags, so this goes through from:, a YAML document in the config's own shape. dqx_config_schema gives the agent the real keys instead of remembered ones.

Something is wrong and you do not know what.

admin.zorgpad.dqx returns 502. Use dqx_doctor and dqx_machine_status before changing anything, and tell me what you found before you fix it.

The cause is rarely where it looks: a probe that can never pass, another backend holding 443, a worker restarting in a loop.

Ask for the plan first

Every prompt here says show me before you write. dqx_init_project refuses any directory that is already a project, so the worst case is a directory you delete.

Without MCP ​

Every fact is available to a tool that can only run a command:

bash
dqx info --json      # this project: sites, checkouts, container paths, services
dqx list --json      # every project on the machine
dqx doctor --json    # findings with a severity, and a verdict
dqx verify --json    # what every declared domain answered

Each payload carries the dqx version, and the shapes only ever gain fields.

In the project's own guidelines ​

dqx ai --write, dqx sync and dqx adopt --write all write the same things, from one function, so none can quietly do less than another:

  • .mcp.json gains dqx's entry, merged under its own key. Laravel Boost merges the same way, so both survive a boost:install.
  • An environment note - where the code runs, what to run in it, which services it has - in the file the project's own AI tooling composes. Never CLAUDE.md where Boost rebuilds that file from .ai/guidelines/*.md, so a Boost project gets a guideline file and anything else gets AGENTS.md.
  • Both CLAUDE.md and AGENTS.md, wherever the project has the first. Claude Code reads one, Codex and Cursor and Zed the other; choosing one leaves some agent seeing nothing.

One of each per checkout, not per project: an agent opens the application it is working on, and a file in the first site's checkout says nothing to an agent sitting in the fourth. Both are silent after the first time, and each replaces only its own marked block.

dqx up says it, dqx ai does it ​

These files live in your repository. dqx up wrote them on every start until v1.37.0, which is a diff nobody asked for from a command whose job is to start containers. So up reports:

text
what an agent needs from dqx is out of date - `dqx ai` says what, `--write` fixes it
bash
dqx ai                           # what it would write, and nothing else
dqx ai --write                   # write it
dqx ai --site crm.example.dqx   # one site; the site you are standing in by default

dqx sync and dqx adopt --write still write them without asking, because both are already rewriting what dqx owns.

Laravel Boost's entry, and the key dqx removes

dqx corrects the Boost entry that is already there, pointing it at dqx php artisan boost:mcp. Rewritten only, never created: a project that does not use Boost never acquires a Boost server.

dqx used to also set BOOST_PHP_EXECUTABLE_PATH=dqx php. It no longer does, and removes the ones it wrote. Boost 2 reads that key on every tool call, in whichever process runs boost:mcp, so dqx php there was sh: exec: dqx: not found inside the container and every Boost tool was dead while the server still connected. Unset, Boost falls back to the php already running it.

text
! .mcp.json runs Boost with the host's php - written by boost:install; --write points it through dqx
update 3 .mcp.json files
refresh .ai/guidelines/dqx-environment.md
unset BOOST_PHP_EXECUTABLE_PATH in .env, .env.example (tracked - commit it)

The removal covers .env, .env.example, .env.testing, .env.testing.example and .env.profile-dev. .env.example is included because it is committed and cp .env.example .env is the first thing an install does. Only that key is removed; every other line, comment and blank stays, including a commented-out one. Nothing production-shaped is ever touched.

The ! line is the state Boost's own installer can still leave behind: with no key set it writes a bare php into .mcp.json, which your editor then launches on the host, against a checkout whose database and services are not there.

What it replaces

26 of this fleet's application repositories carry agent instructions hardcoding

text
docker exec -w /var/www/<domain> <container> php artisan …

That path stops existing at the next domain change, layout conversion or backend switch. dqx exec reads the path from the config instead. dqx doctor reports a file that still hardcodes it, naming the file, and dqx never rewrites somebody else's instructions.

An environment an agent can wreck ​

An agent working on a task wants a running project of its own, not yours:

bash
dqx agent-env "ACME-6479 invoice export"

It copies the project beside itself the way dqx clone does, with its own domains, containers and ports, and hands you a directory to point the agent at. The label in every URL comes from the task you typed.

It does not ask you to stop working. dqx clone refuses while the original runs, because copying a database directory out from under MySQL can catch a half-written page. An agent environment throws that copied database away and migrates its own.

bash
dqx agent-env "ACME-12 payment retry" --with-data    # the original's data, dumped while it runs
dqx agent-env "ACME-12 payment retry" --branch feature/ACME-12

--with-data reads the original's database out with mysqldump --single-transaction, so it still stops nothing. Without it the environment runs the project's own install commands against an empty database, which is seconds rather than minutes.

The agent can make its own

With --allow-write an agent has three tools: dqx_agent_env makes an environment, dqx_agent_env_list says which exist, dqx_agent_env_remove takes one away. The list is free; the other two exist only with the flag, and are marked so Claude asks you first.

dqx_agent_env_remove will only ever take an environment an agent made. A copy you made to work in is yours, and it says so and points at dqx clone --remove. Removal is a move to the trash either way.

Sharing the original's git database ​

By default every checkout in the environment is an ordinary clone with a .git of its own. For feature work that is often not what you want:

bash
dqx agent-env "ACME-6479 invoice export" --worktree

--worktree attaches the repositories as git worktrees of the original's rather than copies. They share one object database, so:

  • a commit the agent makes is in your git log the moment it is made, with no push and no fetch
  • git worktree list in the original says which environments exist and what each is on
  • git refuses the same branch in two places, so two environments cannot quietly diverge on it

Each repository gets its own new branch, named after the task and based on origin/development, or whatever --from names. Not a preference: git refuses a worktree for a branch already checked out, and the original is sitting on the branch you started from.

origin/development rather than each checkout's current branch, because that is where a feature starts: branching off whatever you left a checkout on inherits your half-finished work. The remote one rather than the local, because a development unfetched for a week is not what everybody else has.

Run it from inside a checkout and that checkout is the one attached, which is usually what you mean. From the project root, every repository is attached. Name them to be explicit:

bash
dqx agent-env "ACME-6479 portal and api" --worktree portal.example.dqx,api.example.dqx

With --branch, an existing branch is checked out instead of a new one being made, for reviewing work that is already out there.

For a hotfix, name the branch it belongs on:

bash
dqx agent-env "ACME-91 urgent" --worktree api.example.dqx --from main

main and master are treated as one request, because a fleet accumulates both. A name matching nothing asks which branch to start from, suggesting that repository's own default; where there is nobody to ask, that checkout is copied rather than attached.

A checkout that is not a git repository is always copied, and so is one whose branch is checked out somewhere else. dqx never passes --force.

What happens to the branch

Removing an environment removes the branches it started, in each repository, but only where they hold nothing that is not already on a remote. One with work of its own is kept and named, with the number of commits.

A branch you named yourself with --branch is never touched: the environment borrowed it.

It writes into the original

git worktree add records the worktree in the original's .git/worktrees/, which is exactly why git worktree list can see it. No history and no file of yours is touched, and dqx clone --remove tells the original to forget it again. The plain copy leaves the original untouched entirely, which is why it stays the default.

Getting the work back ​

The checkouts are ordinary git clones with your remotes, so commit and git push origin <branch> works. There is a shorter route that needs no remote:

bash
git fetch ../<project>-<label>/www/<checkout>    # run in the original

The command prints both when it finishes.

Cleaning up ​

Every agent environment is temporary, always. There is no flag to make one permanent.

bash
dqx clone --remove      # inside the environment
dqx clone --prune       # every temporary copy on this machine
dqx clone --list        # what exists, and what each one was made for

Removal is a move to the trash, never rm -rf: an environment can hold a migration somebody wrote at 17:00 and never committed.

dqx v1.78.0