Skip to content

Provide a first-class default/standard toolset + clarify headless allowed_tools semantics #281

Description

@edspencer

Summary

herdctl consumers currently have to hand-enumerate the full built-in tool allowlist to get "a normal, fully-functional agent." There is no default/standard toolset primitive, and under the headless CLI runtime (claude -p) an incomplete allowed_tools is not just a skipped prompt — it is a silent denial of any prompt-requiring tool. Because Claude Code keeps adding built-in tools (e.g. Skill), any hand-typed allowlist silently drifts and breaks over time with no error.

This is a follow-on to #184 (which added the tools availability whitelist). #184 fixed the naming/availability confusion; this issue is about the headless denial semantics and the missing default-toolset primitive — neither of which #184 addresses.

Problem

1. No "standard/default toolset" primitive. There is no wildcard for built-ins, no preset, and no DEFAULT_TOOLS constant anywhere in packages/core/src (grep confirms). To run a normal agent a consumer must either enumerate every built-in by hand, or use bypassPermissions (which herdctl's own docs correctly say to reserve for Docker-isolated agents). Both are bad defaults.

2. Headless (-p) turns an incomplete allowed_tools into a silent denial, not a skipped prompt. The CLI runtime always spawns in print/headless mode and defaults to acceptEdits:

  • packages/core/src/runner/runtime/cli-runtime.ts:138const args: string[] = ["-p"];
  • packages/core/src/runner/runtime/cli-runtime.ts:141permission_mode defaults to acceptEdits
  • packages/core/src/runner/runtime/cli-runtime.ts:157-159allowed_tools is passed straight through to --allowedTools

There is no permission callback / canUseTool / permissionPromptTool wiring in core (grep for all three returns nothing). So under -p, when the model calls a tool that requires approval and is not pre-approved via --allowedTools, there is no one to approve it and it is effectively denied. Under the default acceptEdits mode, only file ops (Read/Write/Edit) auto-approve — so Bash, Task, Skill, TodoWrite, etc. all require pre-approval or they die. This makes allowed_tools a de-facto availability gate for prompt-requiring tools in the headless runtime, which is the opposite of what the docs describe.

3. The docs describe the interactive model only. docs/src/content/docs/configuration/permissions.md:156-158 and :229-233 state that allowed_tools "only controls permission prompts, NOT tool availability." That is true interactively, but there is no warning anywhere that under the CLI/-p runtime an un-pre-approved prompt-requiring tool is effectively denied. A grep of the permissions/agent-config/runner docs for headless / print mode / -p / "effectively denied" returns nothing.

4. Empty-array vs unset is a sharp edge — for tools and allowed_tools. Both fields are gated on ?.length, so an empty array is indistinguishable from unset and the flag is simply not passed:

  • allowed_tools: cli-runtime.ts:157, sdk-adapter.ts:160
  • tools: cli-runtime.ts:167, sdk-adapter.ts:169

So allowed_tools: [] does not produce a tool-less agent — and neither does tools: [] (it is also swallowed by the guard, contrary to what you might expect from the tools whitelist). The only way to actually get tool-lessness today is a denied_tools blocklist; the shipped examples/hello-world/agents/hello-world.yaml proves this by pairing allowed_tools: [] with a full denied_tools list rather than relying on either empty array.

Evidence / repro (the motivating bug)

A @herdctl/core consumer (Paddock) set:

defaults:
  allowed_tools: [Read, Edit, Write, Bash, Glob, Grep, WebFetch, WebSearch]

This list omitted Skill (and Task/TodoWrite/NotebookEdit). Because agents run headless (claude -p) with no approver, every Skill/subagent/todo invocation was denied instantly (~1ms) — no error surfaced to config validation, just silently broken skills. The allowlist had been correct when written; it drifted the moment Claude Code shipped the Skill tool. This is exactly the invisible-drift failure mode: a hand-typed allowlist silently breaks as the built-in toolset evolves.

Proposed solutions (options, not over-prescribed)

(a) A documented, versioned DEFAULT allowlist that herdctl applies when allowed_tools is unset. herdctl tracks Claude Code's evolving built-in toolset so consumers don't have to. This makes "unset" mean "a sensible, current, fully-functional standard agent" instead of "whatever survives headless denial."

(b) A preset/token meaning "all standard built-in tools" — e.g. allowed_tools: [std] or a *-style built-in wildcard — that expands to the current built-in set at run time. Composable with extra entries (allowed_tools: [std, "Bash(git *)"]).

(c) Docs clarification that under the CLI/-p runtime, un-pre-approved prompt-requiring tools are effectively denied (i.e. allowed_tools is a de-facto headless availability gate under default/acceptEdits), plus a note that allowed_tools: [] == unset and tools: [] == unset (both no-ops), and that real tool-lessness needs a denied_tools blocklist or a non-empty tools whitelist.

(a) and (b) are complementary; (c) is worth doing regardless.

Why it belongs in herdctl

Permission/toolset definition is the orchestration layer's job. herdctl already owns the mapping from config to --allowedTools/--tools/--disallowedTools and chooses the headless -p runtime. The gap between the interactive mental model the docs teach and the headless denial the runtime enforces is precisely what forces every consumer to hardcode a full allowlist and then silently drift. Centralizing a tracked default toolset (or a preset token) fixes it once for all consumers.


Refs: #184 (added the tools field), anthropics/claude-code#20242 (allowed_tools naming confusion).

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions