{"record":{"id":"767dd0b7d81c0468","repo":"docker/compose","slug":"can-t-watch-with-action-q-on-service-s-without-a","errorCode":null,"errorMessage":"can't watch with action %q on service %s without a command","messagePattern":"can't watch with action %q on service (.+?) without a command","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/compose/watch.go","lineNumber":428,"sourceCode":"\n\tfor i, trigger := range config.Watch {\n\t\tif !filepath.IsAbs(trigger.Path) {\n\t\t\ttrigger.Path = filepath.Join(baseDir, trigger.Path)\n\t\t}\n\t\tif p, err := filepath.EvalSymlinks(trigger.Path); err == nil {\n\t\t\t// this might fail because the path doesn't exist, etc.\n\t\t\ttrigger.Path = p\n\t\t}\n\t\ttrigger.Path = filepath.Clean(trigger.Path)\n\t\tif trigger.Path == \"\" {\n\t\t\treturn nil, errors.New(\"watch rules MUST define a path\")\n\t\t}\n\n\t\tif trigger.Action == types.WatchActionRebuild && service.Build == nil {\n\t\t\treturn nil, fmt.Errorf(\"service %s doesn't have a build section, can't apply %s on watch\", types.WatchActionRebuild, service.Name)\n\t\t}\n\t\tif trigger.Action == types.WatchActionSyncExec && len(trigger.Exec.Command) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"can't watch with action %q on service %s without a command\", types.WatchActionSyncExec, service.Name)\n\t\t}\n\n\t\tconfig.Watch[i] = trigger\n\t}\n\treturn &config, nil\n}\n\nfunc checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {\n\tfor _, volume := range volumes {\n\t\tif volume.Bind != nil {\n\t\t\trelPath, err := filepath.Rel(volume.Source, watchPath)\n\t\t\tif err == nil && !strings.HasPrefix(relPath, \"..\") {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}","sourceCodeStart":410,"sourceCodeEnd":446,"githubUrl":"https://github.com/docker/compose/blob/ddc4b044b62e9f715212ea4143fa830fac76382f/pkg/compose/watch.go#L410-L446","documentation":"A `develop.watch` trigger uses `action: sync_exec` (or `exec`) but its `exec:` block has no `command`. Compose validates that sync_exec rules carry a command to run after syncing, and fails config load when `trigger.Exec.Command` is empty.","triggerScenarios":"Running `docker compose watch` / `up --watch` with a rule like `{path: ./src, action: sync_exec}` that omits `exec: {command: [...]}`. Raised in pkg/compose/watch.go during watch config validation.","commonSituations":"Renaming `sync` to `sync_exec` without adding an exec block; YAML indentation mistakes that leave `exec:` empty; misunderstanding that sync_exec requires a command while plain sync does not.","solutions":["Add an `exec: {command: [...]}` block to the sync_exec watch rule","Use `action: sync` instead if no command should run after the copy"],"exampleFix":"# before\ndevelop:\n  watch:\n    - path: ./src\n      action: sync_exec\n      target: /app/src\n\n# after\ndevelop:\n  watch:\n    - path: ./src\n      action: sync_exec\n      target: /app/src\n      exec:\n        command: make test","handlingStrategy":"validation","validationCode":"# assert every sync_exec rule declares a command\npython3 - <<'EOF'\nimport yaml, sys\np = yaml.safe_load(open('compose.yaml'))\nfor name, svc in (p.get('services') or {}).items():\n    for rule in ((svc.get('develop') or {}).get('watch') or []):\n        if rule.get('action') in ('sync_exec', 'exec') and not (rule.get('exec') or {}).get('command'):\n            sys.exit(f\"service {name}: sync_exec rule missing exec.command\")\nEOF","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Remember sync_exec = sync + required exec.command; plain sync needs none","Validate the full file with `docker compose config` in CI to catch watch-rule mistakes early"],"tags":["compose","watch","config","exec"],"backgroundTag":null,"analyzedSha":"ddc4b044b62e9f715212ea4143fa830fac76382f","analyzedAt":"2026-08-15T13:31:42.319Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}