{"record":{"id":"0f21d828f0cd724c","repo":"Hmbown/CodeWhale","slug":"custom-sub-agent-requires-a-non-empty-allowed-tool","errorCode":null,"errorMessage":"Custom sub-agent requires a non-empty allowed_tools list","messagePattern":"Custom sub-agent requires a non-empty allowed_tools list","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/subagent/mod.rs","lineNumber":18473,"sourceCode":"///\n/// `allow_shell = false` no longer narrows the tool LIST — the child's\n/// registry simply doesn't register shell tools, which has the same\n/// effect without papering over the parent's choice with a deny-list.\nfn build_allowed_tools(\n    agent_type: &FleetRole,\n    explicit_tools: Option<Vec<String>>,\n    _allow_shell: bool,\n) -> Result<Option<Vec<String>>> {\n    if let Some(tools) = explicit_tools {\n        let mut deduped = Vec::new();\n        for tool in tools {\n            let name = tool.trim();\n            if !name.is_empty() && !deduped.iter().any(|existing: &String| existing == name) {\n                deduped.push(name.to_string());\n            }\n        }\n        if matches!(agent_type, FleetRole::Custom) && deduped.is_empty() {\n            return Err(anyhow!(\n                \"Custom sub-agent requires a non-empty allowed_tools list\"\n            ));\n        }\n        return Ok(Some(deduped));\n    }\n\n    if matches!(agent_type, FleetRole::Custom) {\n        return Err(anyhow!(\n            \"Custom sub-agent requires a non-empty allowed_tools list\"\n        ));\n    }\n\n    // Default: full registry inheritance from the parent. The child sees every\n    // tool the parent has, including the sub-agent management family. The\n    // registry execution guard still blocks approval-gated tools unless the\n    // parent runtime is auto-approved.\n    Ok(None)\n}","sourceCodeStart":18455,"sourceCodeEnd":18491,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tools/subagent/mod.rs#L18455-L18491","documentation":"When a custom (`FleetRole::Custom`) sub-agent declares an allowed_tools list, every entry is trimmed and deduplicated; if nothing survives, the list is empty and configuration is rejected. A custom agent must name at least one tool explicitly — unlike built-in roles, it gets no default toolset.","triggerScenarios":"Creating a custom sub-agent with `allowed_tools: []`, only whitespace/comma entries like `\" \"` or `\",,\"`, or duplicate names that dedupe down to zero.","commonSituations":"Config typo leaving allowed_tools blank; building the list programmatically from an empty source; copying a config template with a placeholder empty list.","solutions":["Add at least one real tool name to allowed_tools","Remove surrounding whitespace/separator-only entries from the list","Use a built-in role instead of Custom if the default tool inheritance is desired"],"exampleFix":"// before\nspawn_subagent(type: \"custom\", allowed_tools: [\"  \", \"read\", \"read\"])\n// after\nspawn_subagent(type: \"custom\", allowed_tools: [\"read\", \"grep\"])","handlingStrategy":"validation","validationCode":"const cleaned = [...new Set((allowedTools ?? []).map(t => t.trim()).filter(Boolean))];\nif (agentType === 'custom' && cleaned.length === 0) throw new Error('allowed_tools must be non-empty for custom agents');","typeGuard":"const hasAllowedTools = (list) => Array.isArray(list) && list.map(s => String(s).trim()).filter(Boolean).length > 0;","tryCatchPattern":null,"preventionTips":["Validate custom agent configs at load time","Reject whitespace-only or duplicate-only tool lists early","Prefer built-in roles when default tool inheritance is wanted"],"tags":["subagent","configuration","validation"],"backgroundTag":"empty-required-field","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}