{"record":{"id":"ae30ba018a6ed275","repo":"github/spec-kit","slug":"invalid-strategy-value-must-be-a-string-got-typ","errorCode":null,"errorMessage":"Invalid strategy value: must be a string, got {type(strategy).__name__}","messagePattern":"Invalid strategy value: must be a string, got (.+?)","errorType":"validation","errorClass":"PresetValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/presets/__init__.py","lineNumber":453,"sourceCode":"            if tmpl[\"type\"] not in VALID_PRESET_TEMPLATE_TYPES:\n                raise PresetValidationError(\n                    f\"Invalid template type '{tmpl['type']}': \"\n                    f\"must be one of {sorted(VALID_PRESET_TEMPLATE_TYPES)}\"\n                )\n\n            # Validate file path safety: must be relative, no parent traversal\n            file_path = tmpl[\"file\"]\n            normalized = os.path.normpath(file_path)\n            if os.path.isabs(normalized) or normalized.startswith(\"..\"):\n                raise PresetValidationError(\n                    f\"Invalid template file path '{file_path}': \"\n                    \"must be a relative path within the preset directory\"\n                )\n\n            # Validate strategy field (optional, defaults to \"replace\")\n            strategy = tmpl.get(\"strategy\", \"replace\")\n            if not isinstance(strategy, str):\n                raise PresetValidationError(\n                    f\"Invalid strategy value: must be a string, \"\n                    f\"got {type(strategy).__name__}\"\n                )\n            strategy = strategy.lower()\n            # Persist normalized value so downstream code sees lowercase\n            if \"strategy\" in tmpl:\n                tmpl[\"strategy\"] = strategy\n            if strategy not in VALID_PRESET_STRATEGIES:\n                raise PresetValidationError(\n                    f\"Invalid strategy '{strategy}': \"\n                    f\"must be one of {sorted(VALID_PRESET_STRATEGIES)}\"\n                )\n            if tmpl[\"type\"] == \"script\" and strategy not in VALID_SCRIPT_STRATEGIES:\n                raise PresetValidationError(\n                    f\"Invalid strategy '{strategy}' for script: \"\n                    f\"scripts only support {sorted(VALID_SCRIPT_STRATEGIES)}\"\n                )\n","sourceCodeStart":435,"sourceCodeEnd":471,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/presets/__init__.py#L435-L471","documentation":"Raised while validating a template entry in preset.yml: the optional 'strategy' field on a template must be a YAML string (e.g. replace, prepend, append, wrap), but some other YAML type was found. The validator reports the offending Python type name so you can see whether YAML parsed your value as an int, bool, list, or dict. It is thrown before any strategy comparison, so nothing is installed.","triggerScenarios":"A template entry in preset.yml whose 'strategy' key is a non-string YAML scalar or collection, e.g. `strategy: [replace]` (list), `strategy: {name: replace}` (dict), `strategy: 10` (int), or `strategy: true` (bool). YAML booleans are a frequent trap: an unquoted value is parsed by YAML into a bool/int, not a string.","commonSituations":"Hand-editing preset.yml and writing `strategy: yes`/`strategy: no` (parsed as bool), copying a JSON-style array from docs, or generating the manifest programmatically and inserting a Python object instead of a str.","solutions":["Open preset.yml and set the template's strategy to a quoted string: `strategy: \"replace\"` (valid values: append, prepend, replace, wrap).","If the strategy is optional for your case, delete the key entirely — it defaults to \"replace\".","If you generate preset.yml with yaml.safe_dump, ensure you pass a str value, not a bool/int/list."],"exampleFix":"# before (preset.yml)\ntemplates:\n  - name: build\n    type: script\n    file: scripts/build.sh\n    strategy: true   # YAML bool\n\n# after\n    strategy: \"replace\"","handlingStrategy":"type-guard","validationCode":"import yaml\n\nm = yaml.safe_load(open(\"preset.yml\"))\nfor t in m.get(\"templates\", []):\n    s = t.get(\"strategy\", \"replace\")\n    assert isinstance(s, str), f\"template {t.get('name')}: strategy must be a string, got {type(s).__name__}\"","typeGuard":"def is_valid_strategy_type(value) -> bool:\n    return isinstance(value, str)","tryCatchPattern":"try:\n    manager.install_from_directory(src, version)\nexcept PresetValidationError as e:\n    if \"Invalid strategy value\" in str(e):\n        # fix preset.yml strategy field, re-run\n        ...","preventionTips":["Always quote strategy values in YAML: strategy: \"replace\".","Never use YAML bare yes/no/true/false as a strategy.","Lint preset.yml in CI before publishing the preset."],"tags":["preset","validation","yaml","strategy"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}