Skip to content

Configuring .drupalaibp/

A config is a *.json file inside a .drupalaibp/ directory in the place you launch the installer from. When one is present, the installer reads it and pre-configures the whole build — project type, coding agents, agent skills, provisioning (Composer packages, modules, recipes, DDEV add-ons), custom questions, and hook scripts fired at named build events.

# in a directory containing .drupalaibp/config.json
bash <(curl -fsSL https://project.pages.drupalcode.org/one_line_installer/drupalaibp)

Upgrading from 1.x? The single root .drupalaibp.json is no longer read. Move it into the directory: mkdir .drupalaibp && git mv .drupalaibp.json .drupalaibp/config.json.

The structural choices (profile, agents, skills, deny rules, provisioning) come from the config and aren't prompted. The config's questions are still asked interactively, each with its default pre-filled, so you can review or adjust them. Add --yolo to take every default silently and run fully unattended.

There are two complete, runnable examples in docs/examples/ — a Drupal site with provisioning, and a non-Drupal Node/Storybook project.

Where a config lives, and picking one

Every *.json directly under .drupalaibp/ is a named config — the file's basename (without .json) is its name. The simplest project has a single .drupalaibp/config.json.

  • One config in .drupalaibp/ is used automatically.
  • Several configs — pass --config <name> (the basename, no .json) to pick one, or choose interactively from the list. In an unattended run (--yolo, or a source loaded with no terminal) a multi-config directory with no --config is a hard error that names the available configs.

The same .drupalaibp/ directory can be loaded from several places — the launch directory (auto-detected), a positional source drupalaibp <source>, or the --config-path/--config-url/--global-config flags. See the setup-site reference for the full source list and flag equivalences.

Git sources and refs (--config-url)

A git URL source (drupalaibp <url> or --config-url <url>) is cloned and its .drupalaibp/ read. Pin a branch, tag, or ref two ways:

  • an @<ref> suffix on the URL — drupalaibp https://git.example.org/acme/site@1.2.x;
  • the --ref <ref> flag — --config-url https://git.example.org/acme/site --ref 1.2.x.

A ref may be a branch name, a tag, a full refs/… ref, or !<iid> for a GitLab merge request (!64refs/merge-requests/64/head).

Paths inside a config resolve relative to the config file

Because the config file sits inside .drupalaibp/, every config-relative path — hooks[].script, copy_paths, db_import_path — resolves against that .drupalaibp/ directory (CONFIG_BASEDIR). Reference a script or asset that lives next to the config as "./seed.sh" (→ .drupalaibp/seed.sh), and use a leading "../" to reach the repo root. (This differs from the pre-2.x root .drupalaibp.json, where such paths were relative to the repo root.)

Three things to get right

!!! warning "1. Drupal projects need a Drupal ddev_project_type" For a Drupal build (is_drupal: true, the default), ddev_project_type must be a Drupal type — "drupal11" (default), "drupal10", "drupal12", and so on. It is passed straight to ddev config --project-type. Using "php" or another generic type for a Drupal site will not configure Drupal correctly.

!!! warning "2. Non-Drupal builds ignore the Drupal-only keys" If you set is_drupal: false, the keys profile, site_template, install_modules and recipes do nothing — there's no composer create-project, drush, or config export. A non-Drupal build still sets up DDEV, coding agents, skills, ddev_addons and hooks; drive the actual project setup with hooks (e.g. npm/composer commands run in the web container). Set ddev_project_type to a non-Drupal DDEV type such as "php".

!!! tip "3. Keep hook scripts next to the config" Store hook scripts in the same .drupalaibp/ directory as the config and reference them as "./<name>.sh" (paths resolve relative to the config file, which lives inside .drupalaibp/). Paths must be whitespace-free and are copied into the new project root before they run. Use a leading "../" only if a script deliberately lives at the repo root.

Full schema

{
  // project shape
  "ddev_project_type": "drupal11",   // Drupal: drupal11/drupal10/drupal12…  Non-Drupal: e.g. "php"
  "is_drupal": true,                 // false => skip all Drupal steps
  "docroot": "web",                  // ddev config --docroot. Default: "web" (Drupal) / "." (non-Drupal)
  "launch_path": "",                 // ddev launch argument override, e.g. ":6006" for a dev-server port

  // Drupal-only (ignored when is_drupal:false)
  "profile": "cms",                  // cms (default) | standard | minimal | template
  "site_template": "",               // site-template project name, when profile=template
  "install_modules": ["ai", "token"],
  "recipes": ["drupal/drupal_cms_blog"],   // apply-only (drush recipe) — fetch contrib recipes via composer_projects

  // agents & skills
  "agents": ["claude"],              // claude | codex | opencode — empty = bring your own
  "agent_auto_mode": false,          // true => bake each agent's unattended/auto-approval flag into `ddev <agent>`
  "extra_scripts": ["tool_api"],     // ids of "Extra Drupal Installations" (src/lib/extras/) to run
  "skills": [
    "obra/superpowers",              // "owner/repo" = all skills in the repo
    { "repo": "ivanboring/drupal-module-finder", "skills": ["drupal-module-finder"] }
  ],
  "deny_commands": ["git push", "ssh"],

  // composer
  "composer_stability": "dev",       // minimum-stability (prefer-stable always on)
  "composer_with_all_dependencies": false, // true => composer_projects require gets --with-all-dependencies (-W)
  "composer_allow_plugins": ["tbachert/spi"],
  "repositories": [
    { "type": "vcs", "url": "https://github.com/acme/private-module" }
  ],
  "composer_projects": ["drupal/ai:^1.1", "acme/private-module:dev-main"],

  // ddev add-ons
  "ddev_addons": ["ddev/ddev-redis"],

  // AGENTS.md additions
  "agents_md": ["Project rule appended to the AGENTS.md managed block."],

  // extra files/directories copied into the project root (for hook-consumed
  // assets that aren't scripts themselves), same path convention as hooks —
  // relative to the config file inside .drupalaibp/
  "copy_paths": ["./seed-assets"],

  // --config-url existing-codebase mode: the clone already IS the Drupal
  // codebase (its own composer.json/web/), so `ddev composer install` +
  // `ddev import-db` stand it up instead of scaffolding. Set at most ONE —
  // db_import: true if the dump is supplied at runtime via --db-path (too
  // large/sensitive to commit); db_import_path if it's committed in the repo.
  "db_import": false,
  "db_import_path": "",              // e.g. "db/minimal.sql.gz", relative to the config file

  // welcome-message additions (replace wins if both are set)
  "append_welcome_text": ["Read CONTRIBUTING.md before your first commit."],
  "replace_welcome_text": [],

  // custom questions (answers -> .ddev/.env)
  "questions": [
    { "env": "OPENAI_API_KEY", "label": "OpenAI API key", "type": "password" },
    { "env": "SITE_FLAVOR", "label": "Flavor", "type": "single-select-options",
      "select_options": ["blog", "shop"], "default": "blog" },
    { "env": "SITE_ENV", "type": "text", "value": "demo" }
  ],

  // hook scripts at build events (paths relative to the config file in .drupalaibp/)
  "hooks": {
    "ddev_started":       [ { "script": "./seed.sh",  "weight": 10, "where": "web" } ],
    "coding_agent_added": [ { "script": "./agent.sh", "weight": 0,  "where": "host" } ]
  }
}

Key reference

Key Applies to Notes
ddev_project_type all ddev config --project-type. Drupal builds must use drupal11/drupal10/drupal12/… (see point 1).
is_drupal all default true. false skips every Drupal step.
docroot all ddev config --docroot. Default "web" (Drupal) / "." (non-Drupal).
launch_path all ddev launch argument override, e.g. ":6006" for a dev-server daemon on its own port. Default: launches the project root.
profile Drupal cms (default), standard, minimal, template.
site_template Drupal site-template project short name (with profile: template).
install_modules Drupal enabled with drush.
recipes Drupal apply-only (drush recipe); never fetched. A drupal/<name> entry is applied from ../recipes/<name> — fetch it via composer_projects. Any other entry (e.g. core/recipes/standard) is passed to drush verbatim, so a core / already-present recipe applies with no fetch.
agents all coding agents installed into the container.
agent_auto_mode all default false. true prepends each agent's own unattended flag to its ddev <agent> wrapper (Claude --permission-mode auto, Codex --dangerously-bypass-approvals-and-sandbox, OpenCode --auto). Config-only — no CLI flag or prompt.
extra_scripts all ids of "Extra Drupal Installations" (src/lib/extras/extra_<id>.sh) to run — the config equivalent of --extras.
skills all single list; "owner/repo" (all) or {repo, skills:[…]} (["*"] = all).
deny_commands all commands the agent(s) may never run.
composer_stability composer minimum-stability; default dev.
composer_with_all_dependencies composer default false. true adds --with-all-dependencies (-W) to the composer_projects require, so Composer may update the whole dependency tree (needed when a package — e.g. a recipe you fetch here — requires newer shared deps).
composer_allow_plugins composer plugins to trust; is_drupal adds core-dev/ABP defaults.
repositories composer registered before composer_projects.
composer_projects composer fetch-only — verbatim composer require args, version constraints allowed (drupal/ai:^1.1); never applied/enabled. Fetch a contrib recipe here, then apply it via recipes.
ddev_addons all added after the site is built, then one ddev restart.
agents_md all paragraphs appended to the managed AGENTS.md block.
copy_paths all extra files/directories copied into the project root, same path convention as hooks[].script — for hook-consumed assets that aren't scripts themselves.
db_import --config-url default false. true selects existing-codebase mode (the clone IS the Drupal codebase) with the dump supplied at runtime via --db-path. Mutually exclusive with db_import_path.
db_import_path --config-url a dump committed in the repo, path relative to the config file — same existing-codebase mode as db_import, but resolved automatically, no --db-path needed.
append_welcome_text all paragraph(s) appended to the end of the print_welcome summary. Ignored if replace_welcome_text is set.
replace_welcome_text all paragraph(s) that replace the entire print_welcome summary (no admin login, no handy commands, no DDEV footer) — takes precedence over append_welcome_text.
questions all see below; answers → .ddev/.env.
hooks all see below.

Welcome-text placeholders and colors

Both append_welcome_text and replace_welcome_text support {TOKEN} substitution:

Built-in tokens:

Token Value
{PROJECT_NAME} the DDEV project name (lowercased directory name)
{SITE_URL} https://<project>.ddev.site
{DRUPAL_USERNAME} admin
{DRUPAL_PASSWORD} admin
{DRUPAL_LOGIN_URL} {SITE_URL}/user

Env-var tokens: any questions[].env answer is addressable by its own name, e.g. {OPENAI_API_KEY}, {SITE_FLAVOR}.

{
  "append_welcome_text": [
    "Environment: {SITE_ENV}  •  Flavor: {SITE_FLAVOR}",
    "Log in at {DRUPAL_LOGIN_URL} with {DRUPAL_USERNAME} / {DRUPAL_PASSWORD}."
  ]
}

!!! warning "Unknown tokens print literally" A {TOKEN} that isn't a built-in or a questions[].env name is left untouched in the output (not emptied) — this surfaces a typo instead of silently hiding it.

!!! warning "Secrets land on the terminal" A password-type question's answer, if referenced by a welcome-text placeholder, is echoed to the terminal in the final build summary. Don't reference a genuinely sensitive value this way unless you're fine with it appearing in that output.

Colors: {BOLD}, {DIM}, {GREEN}, {YELLOW}, {RED}, {RESET} expand to the installer's own color codes when connected to a real terminal, and to nothing when output isn't a TTY (piped/redirected), so logs stay clean. Every paragraph also gets an automatic trailing reset, so an unclosed color token can't bleed into the next paragraph or the rest of the terminal.

{
  "append_welcome_text": "{BOLD}{GREEN}Welcome to the ACME demo!{RESET} Docs: https://example.com/handbook"
}

Questions

Each question object uses env (the environment-variable name — a valid identifier), label, type, and optionally default, select_options, value, depends_on.

  • Types: text, password, single-select-options, multi-select-options.
  • select_options — the choice list for the two select types.
  • default — pre-fills an editable prompt (still shown).
  • valuepre-answers the question, so it is not shown. Use default (not value) if you want a question prompted with a suggestion.
  • Answers land in .ddev/.env (mode 600, gitignored). Never put a real secret in value — leave it for the user to type.

Conditional questions (depends_on)

A question can be shown only when an earlier question's answer matches a condition:

{
  "env": "REDIS_TTL",
  "label": "Redis cache TTL (seconds)",
  "type": "text",
  "default": "3600",
  "depends_on": { "env": "USE_REDIS", "equals": "yes" }
}
  • depends_on.env must name a question that appears earlier in the questions array. A forward reference, self-reference, or a name that doesn't match any earlier question is invalid: it's ignored with a warning and the question is asked unconditionally instead of being dropped.
  • Condition kinds (exactly one required): "equals": "<value>", "not_equals": "<value>", or "in": ["a", "b", ...] (the dependency's answer is one of the listed values — useful when a select has more than two meaningful options).
  • A question whose condition isn't met is not shown, and its ANSWERS value is set to an empty string — still written to .ddev/.env as KEY="", so hooks and welcome-text tokens can test for emptiness rather than the variable being entirely undefined.
  • value still short-circuits a question before depends_on is ever evaluated for it — a pre-answered question is never conditionally skipped, it's simply always answered from value.

!!! warning "Any depends_on disables .ddev/.env reuse for the whole block" Normally a question already answered in a previous run (its key already present in .ddev/.env) is reused silently instead of being re-prompted — this is what makes a re-run in management mode not re-ask everything. A conditional question's visibility has to reflect the current answer of its dependency, not a possibly-stale one from a previous run — so the presence of any depends_on anywhere in a config's questions disables that reuse for the entire array, on every run, including questions with no depends_on of their own and password questions. A config with no conditional questions is unaffected. If you add one conditional question to a config, every question in that config (including secrets) will be re-prompted on every future re-run of that config.

Hooks and events

Each event name maps to a list of { script, weight, where }:

  • weight — integer; lower runs first.
  • wherehost, web (default; runs in the DDEV web container via ddev exec), or a named DDEV service.

!!! warning "Pre-start hooks must use where: host" web/service hooks need the container running. A hook on a pre-start event (before_ddev_start, ddev_addon_added, coding_agent_added, or anything before ddev_started) must set "where": "host".

!!! warning "Question answers are only in the container" Answers from questions are written to .ddev/.env, which DDEV injects into the web container — not the host. A "where": "web" hook sees them as environment variables ($OPENAI_API_KEY, …); a "where": "host" hook does not. If a hook needs a question's answer, run it in web.

Available events: before_<step> / <step>_done for every pipeline step, plus the semantic events before_ddev_start, ddev_started, coding_agent_added, ddev_addon_added, skill_added, and ask_questions. The most common are ddev_started (the site is up — run drush/composer/npm in web) and coding_agent_added (host-side — tweak AGENTS.md).

!!! note "Hook script and copy_paths staging timing (Drupal fresh-scaffold builds)" For is_drupal: true fresh-scaffold builds (without db_import or db_import_path), copy_paths and most hook scripts are staged after Drupal scaffolding completes (create_drupal_site_done), not before. This is because ddev composer create-project refuses a non-empty project directory.

**Exception: hook scripts on early events.** A hook script registered on an
event that fires **before** Drupal's scaffold
(`before_ddev_start`, `ddev_started`, `coding_agent_added`,
`ask_questions`, or `before_create_drupal_site` itself)
is staged early, but physically lives **only** at
`.ddev/drupalaibp-hooks/<configured-path>` in the final scaffolded
project — **not** at the `script` path written in the config.
This is a real, user-visible difference from every other hook script
(which does land at its configured path in the project root):
an early hook is classified as such and never additionally copied to where
a late hook would be. If you inspect the scaffolded project, an early
hook's script will only be found under `.ddev/`, not in the root.

**Existing-codebase mode and non-Drupal builds are unaffected:**
When `db_import` or `db_import_path` is set, or when `is_drupal: false`,
all hook scripts and `copy_paths` use the original timing — no
deferral, no `.ddev/`-only staging.

Importing Drupal config (.drupalaibp/config/)

Drupal-only. Drop a .drupalaibp/config/ directory (a config/ subdirectory inside .drupalaibp/, alongside the config JSON) containing exported Drupal config YAML, and after the site is installed and provisioned the installer runs a partial drush config:import from it:

my-launch-dir/
└── .drupalaibp/
    ├── config.json             # the installer config
    └── config/
        └── system.site.yml     # e.g. override the site name

It's a partial import — the provided config is applied on top of the installed site; config not present in the directory is left alone (nothing is deleted). This runs after provision_from_config, so config that references modules from install_modules imports cleanly. Ignored for is_drupal: false builds and when the directory has no *.yml. See docs/examples/drupal/ for a working system.site.yml.

Good to know

  • selenium / toolbelt / dtk are not flags — they're ordinary provisioning: Selenium as a ddev_addons entry (a built-in handler makes Chrome headed with noVNC on port 7900), Toolbelt/DTK as composer_projects + install_modules.
  • AI Best Practices + core-dev (drupal/ai_best_practices, drupal/core-dev, and a starter phpunit.xml) are built-in defaults for every is_drupal: true build — you don't list them. ai_best_practices is installed and its Composer plugin allow-listed automatically.
  • The experimental Tool API + Toolbelt (drupal/tool + drupal/tool_belt, so agents can run Drupal actions via drush tool:*) is suggested during an interactive build (or --tool-api). In a config, add it through composer_projects + install_modules (+ tbachert/spi in composer_allow_plugins).
  • Build order: DDEV starts vanilla, the Drupal site is created, then ddev_addons are added and applied with a single restart (some add-ons write files outside .ddev/ that would otherwise break composer create-project).
  • To replace the whole AGENTS.md block content, drop an overrides/agents_md.json next to the config.

Examples

See the runnable examples and their hook scripts under docs/examples/:

  • docs/examples/drupal/is_drupal: true, Drupal CMS on Drupal 11, with a private repo, version-constrained composer projects, modules, a recipe, the Selenium + Redis add-ons, password + select questions, and .drupalaibp/ hook scripts on ddev_started and coding_agent_added.
  • docs/examples/storybook/is_drupal: false, a Node/Storybook project built entirely through where: web npm hooks; every question type drives the npm setup.