How to Create Custom DDEV Commands with Project-Specific Shell Scripts: A Complete Guide
You can extend DDEV's CLI by placing executable Bash scripts in .ddev/commands/host/ for host-side execution or .ddev/commands/<service>/ for container-side execution, using header annotations like ## Usage: and ## Description: to define the command interface.
DDEV provides a powerful extension mechanism that lets you add project-specific or global CLI commands without modifying the core codebase. These custom commands integrate seamlessly with the ddev binary, appearing in help output and supporting flags, autocomplete, and container execution. According to the ddev/ddev source code, the system discovers scripts during ddev start, parses their metadata headers, and registers them as first-class Cobra subcommands.
How DDEV Discovers and Loads Custom Commands
The command discovery system in pkg/ddevapp/command_loader.go (specifically the loadCustomCommands function) scans specific directories for executable files. When you run ddev start, DDEV walks both the project-local .ddev/commands/* hierarchy and the global $HOME/.ddev/commands/* directory, looking for scripts with executable permissions.
After discovery, DDEV parses the header annotations in pkg/ddevapp/customcommand.go. The parser extracts lines beginning with ## to build command metadata, including the command name derived from the ## Usage: line—not the filename. This allows flexible file naming while maintaining clean CLI syntax. Container-side scripts are then executed via the wrapper in pkg/dockerutil/exec.go, which handles docker exec or Mutagen-aware execution depending on your project configuration.
Creating Host-Side Custom Commands
Host-side commands run directly on your development machine, making them ideal for launching IDEs, interacting with host binaries, or performing file system operations outside containers.
Place your script in .ddev/commands/host/ and ensure it is executable:
#!/usr/bin/env bash
## Description: Open PhpStorm in the current project
## Usage: phpstorm
## OSTypes: darwin,linux
## HostBinaryExists: /Applications/PhpStorm.app
open -a PhpStorm.app "${DDEV_APPROOT}"
Key implementation details:
-
The
## Usage: phpstormannotation defines the subcommand name (ddev phpstorm), regardless of the script's filename. -
OSTypesrestricts visibility to specific operating systems (valid values includedarwin,linux, andwindows). -
HostBinaryExistsprevents the command from appearing in help output when the specified binary is missing on the host.
Save this as .ddev/commands/host/phpstorm, run chmod +x .ddev/commands/host/phpstorm, then execute ddev restart to register the command.
Creating Container-Side Custom Commands
Container-side commands execute inside a specific Docker Compose service, giving you access to the container's filesystem, installed tools, and environment variables. These scripts live in .ddev/commands/<service>/, where <service> matches a service name from your docker-compose.yaml (commonly web, db, or solr).
Here is a container-side example that tails a Solr log file with optional flag support:
#!/usr/bin/env bash
## Description: Tail the Solr log file
## Usage: solrtail
## Example: ddev solrtail -n 50
## Flags: [{"Name":"lines","Shorthand":"n","Usage":"Number of lines to show","DefValue":"10"}]
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--lines) LINES="$2"; shift 2 ;;
*) shift ;;
esac
done
LINES=${LINES:-10}
tail -n "${LINES}" /opt/solr/server/logs/solr.log
Important characteristics:
-
The
## Flagsannotation accepts a JSON array defining command-line flags that DDEV exposes in its help system and passes through to your script. -
Container scripts use the container's internal filesystem layout, ensuring consistent behavior across macOS, Linux, and Windows hosts.
-
DDEV injects the script into the target container via the execution logic in
pkg/dockerutil/exec.go, handling stdin/stdout bridging automatically.
Using Global Commands Across All Projects
Global commands reside in $HOME/.ddev/commands/ and are automatically copied into every project during ddev start. This lets you share utilities across repositories without committing them to each project's Git history.
To install a global command from DDEV's built-in examples:
mkdir -p ~/.ddev/commands/host
cp $(git rev-parse --show-toplevel)/pkg/ddevapp/global_dotddev_assets/commands/host/mysqlworkbench.example ~/.ddev/commands/host/mysqlworkbench
chmod +x ~/.ddev/commands/host/mysqlworkbench
After running ddev start once in any project, the mysqlworkbench command becomes available in that project's context. The original example template is stored at pkg/ddevapp/global_dotddev_assets/commands/host/mysqlworkbench.example in the ddev/ddev repository. Global commands support the same annotation system as project-specific scripts, including CanRunGlobally filters to prevent global commands from appearing in incompatible project types.
Essential Annotations and Configuration
The header parsing logic in pkg/ddevapp/customcommand.go supports multiple annotations that control command behavior and visibility:
-
## Usage:(Required) Defines the subcommand name as it appears inddev <command>. -
## Description:(Required) Provides the help text shown inddev -houtput. -
## Example:Shows usage examples in help text. -
## Flags:JSON array defining CLI flags withName,Shorthand,Usage, andDefValuefields. -
## OSTypes:Comma-separated list limiting command visibility to specific host operating systems. -
## ProjectTypes:Restricts the command to specific project types (e.g.,drupal8,wordpress,laravel). -
## HostBinaryExists:Hides the command if the specified path does not exist on the host. -
## AutocompleteTerms:Provides static autocomplete suggestions for shell completion.
For the full annotation specification, refer to docs/content/users/extend/custom-commands.md in the DDEV repository.
Summary
-
DDEV scans
.ddev/commands/host/and.ddev/commands/<service>/duringddev startto discover project-specific scripts, while$HOME/.ddev/commands/stores global utilities. -
The
## Usage:annotation determines the CLI subcommand name, not the filename. -
Host commands execute on the local machine; container commands run inside the specified Docker service via
pkg/dockerutil/exec.go. -
Annotations like
OSTypes,ProjectTypes, andHostBinaryExistsfilter command visibility based on environment context. -
The
## Flagsannotation enables native CLI flag support with automatic help generation. -
You must make scripts executable (
chmod +x) and restart the project (ddev restart) for changes to take effect.
Frequently Asked Questions
What determines the name of my custom DDEV command?
The command name comes from the ## Usage: annotation within your script's header, not the filename. For example, a script named ide-launcher containing ## Usage: phpstorm registers as ddev phpstorm. This allows you to name files descriptively while maintaining simple CLI commands.
Why doesn't my custom command appear in ddev -h output?
First, verify the script is executable with chmod +x. Next, check that you have run ddev start or ddev restart since creating the file, as the loadCustomCommands function in pkg/ddevapp/command_loader.go only scans directories during project startup. Finally, confirm you haven't restricted visibility with annotations like OSTypes or HostBinaryExists that might hide the command on your current system.
Can I pass arguments and flags to custom commands?
Yes. Use the ## Flags: annotation to define structured flags that DDEV will parse and display in help text. For arbitrary arguments, simply reference standard Bash variables like $1, $2, or $@ in your script. Container-side scripts receive these arguments exactly as passed on the host command line.
Where should I store commands that need to work across all my DDEV projects?
Place global commands in $HOME/.ddev/commands/host/ or $HOME/.ddev/commands/<service>/. DDEV copies these files into each project during ddev start, making them available in any project context without committing them to individual repositories. Global commands can use the CanRunGlobally annotation to control whether they appear in every project or only specific project types.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →