Summary
TAKT Phase 2 report generation is defined as a no-tools phase and rejects any provider tool_use / tool_result event.
With the Claude headless provider, however, the explicit no-tools intent represented by:
allowedTools: []
permissionMode: 'readonly'
is not currently preserved as a no-tools Claude Code execution boundary.
This can result in Claude emitting a tool such as Bash during report generation, which TAKT then correctly rejects:
ReportPhaseToolCallError:
Report phase does not allow tool calls, but provider emitted tool "Bash".
I observed this in a downstream workflow using TAKT 0.64.1.
Versions inspected
The relevant execution path was inspected in:
- TAKT 0.64.1
- TAKT 0.65.0
- current
main
The report retry tests also pass on the released versions checked:
0.64.1: 46 / 46
0.65.0: 50 / 50
so this does not appear to be caused by missing report retry machinery.
Relevant TAKT behavior
Phase 2 uses an explicit empty tool allowlist.
The fresh-session retry path passes:
and OptionsBuilder resolves report/resume execution as readonly with an empty allowlist.
However, the Claude headless adapter currently emits --allowed-tools only for a non-empty list:
if (!isStrictReadonly && options.allowedTools && options.allowedTools.length > 0) {
args.push('--allowed-tools', options.allowedTools.join(','));
}
Therefore:
does not itself produce a Claude CLI no-tools restriction.
In addition, the readonly Phase 2 base options retain the step's resolved MCP server set.
TAKT already has a stricter Claude isolation path via internalAgentIsolation === 'strict-readonly', which applies restrictions including:
--tools ""
--strict-mcp-config
--setting-sources ""
--disable-slash-commands
Ordinary Phase 2 report execution does not currently use that isolation path.
Minimal Claude Code observations
Tested with:
Claude Code 2.1.273
model: Sonnet
permission mode: default
Fresh session without tool restriction
Bash was present in the initialization tool set.
When explicitly instructed to invoke Bash, Claude emitted the tool call and it executed successfully.
Fresh session with --tools ""
Bash was absent from the initialization tool set.
When explicitly instructed to invoke Bash, Claude returned text explaining that Bash was unavailable instead of emitting a Bash tool call.
This suggests --tools "" is relevant to suppressing built-in Claude Code tools in a fresh session.
Resumed session observation
I also resumed a session in which Bash had previously been available while applying:
In that run, Claude still emitted a Bash tool-use attempt, but Claude Code rejected execution:
No such tool available: Bash.
Bash is disabled for this session
I would treat this only as an observed resume-session behavior.
It also makes TAKT's existing fresh-session retry useful: a resumed attempt can fail closed, then retry without depending on that session.
MCP observation
In the fresh --tools "" test, built-in tools such as Bash were removed, but configured MCP tools were still visible.
TAKT's report-phase contract rejects all tool use, not only built-in Claude tools.
TAKT also currently carries the resolved MCP server set into readonly Phase 2 execution.
A complete report-phase no-tools boundary therefore needs to account for both:
- Claude Code built-in tools
- MCP-provided tools
Expected behavior
An explicit empty tool list should remain semantically distinct through the provider boundary:
allowedTools === undefined
-> normal/provider-default behavior
allowedTools.length > 0
-> explicitly constrained tool set
allowedTools.length === 0
-> explicit no-tools execution
For report generation, the resulting Claude execution context should expose no callable tools to the model.
Actual behavior
For Claude headless report execution, the explicit empty tool list currently does not select an execution boundary that removes all callable tools.
Phase 2 also retains resolved MCP capabilities.
If Claude emits a tool event, TAKT's report guard correctly rejects the attempt.
Suggested direction
I think the existing fail-closed ReportPhaseToolCallError should remain unchanged.
The issue appears to be at the provider capability boundary: Phase 2's explicit no-tools contract needs to map to a Claude execution context that suppresses both built-in and MCP tools.
TAKT already has a stricter Claude isolation mechanism for internal read-only agents, and Phase 3 has an existing precedent for removing tool/MCP access. Reusing or adapting those abstractions for report execution may be preferable to introducing a separate CLI special case.
I would leave the exact implementation to the maintainers because session reuse, MCP configuration, Skills/settings discovery, and provider isolation semantics interact here.
Suggested regression coverage
Provider-level tests could distinguish:
allowedTools === undefined
allowedTools.length > 0
allowedTools.length === 0
and verify that the explicit empty case used by report generation produces no callable built-in or MCP tools.
A report-phase integration test could additionally cover:
- Phase 1 Claude execution with normal tools.
- A resumed Phase 2 attempt that emits a tool event.
- Existing fail-closed detection.
- Fresh-session retry with the intended no-tools capability boundary.
- Successful report generation without weakening the report guard.
Related
This appears analogous to earlier provider capability-boundary work:
This issue appears to be the Claude headless counterpart of the same general no-tools contract boundary.
Summary
TAKT Phase 2 report generation is defined as a no-tools phase and rejects any provider
tool_use/tool_resultevent.With the Claude headless provider, however, the explicit no-tools intent represented by:
is not currently preserved as a no-tools Claude Code execution boundary.
This can result in Claude emitting a tool such as
Bashduring report generation, which TAKT then correctly rejects:I observed this in a downstream workflow using TAKT 0.64.1.
Versions inspected
The relevant execution path was inspected in:
mainThe report retry tests also pass on the released versions checked:
so this does not appear to be caused by missing report retry machinery.
Relevant TAKT behavior
Phase 2 uses an explicit empty tool allowlist.
The fresh-session retry path passes:
and
OptionsBuilderresolves report/resume execution as readonly with an empty allowlist.However, the Claude headless adapter currently emits
--allowed-toolsonly for a non-empty list:Therefore:
does not itself produce a Claude CLI no-tools restriction.
In addition, the readonly Phase 2 base options retain the step's resolved MCP server set.
TAKT already has a stricter Claude isolation path via
internalAgentIsolation === 'strict-readonly', which applies restrictions including:Ordinary Phase 2 report execution does not currently use that isolation path.
Minimal Claude Code observations
Tested with:
Fresh session without tool restriction
Bashwas present in the initialization tool set.When explicitly instructed to invoke Bash, Claude emitted the tool call and it executed successfully.
Fresh session with
--tools ""Bashwas absent from the initialization tool set.When explicitly instructed to invoke Bash, Claude returned text explaining that Bash was unavailable instead of emitting a Bash tool call.
This suggests
--tools ""is relevant to suppressing built-in Claude Code tools in a fresh session.Resumed session observation
I also resumed a session in which Bash had previously been available while applying:
In that run, Claude still emitted a
Bashtool-use attempt, but Claude Code rejected execution:I would treat this only as an observed resume-session behavior.
It also makes TAKT's existing fresh-session retry useful: a resumed attempt can fail closed, then retry without depending on that session.
MCP observation
In the fresh
--tools ""test, built-in tools such asBashwere removed, but configured MCP tools were still visible.TAKT's report-phase contract rejects all tool use, not only built-in Claude tools.
TAKT also currently carries the resolved MCP server set into readonly Phase 2 execution.
A complete report-phase no-tools boundary therefore needs to account for both:
Expected behavior
An explicit empty tool list should remain semantically distinct through the provider boundary:
For report generation, the resulting Claude execution context should expose no callable tools to the model.
Actual behavior
For Claude headless report execution, the explicit empty tool list currently does not select an execution boundary that removes all callable tools.
Phase 2 also retains resolved MCP capabilities.
If Claude emits a tool event, TAKT's report guard correctly rejects the attempt.
Suggested direction
I think the existing fail-closed
ReportPhaseToolCallErrorshould remain unchanged.The issue appears to be at the provider capability boundary: Phase 2's explicit no-tools contract needs to map to a Claude execution context that suppresses both built-in and MCP tools.
TAKT already has a stricter Claude isolation mechanism for internal read-only agents, and Phase 3 has an existing precedent for removing tool/MCP access. Reusing or adapting those abstractions for report execution may be preferable to introducing a separate CLI special case.
I would leave the exact implementation to the maintainers because session reuse, MCP configuration, Skills/settings discovery, and provider isolation semantics interact here.
Suggested regression coverage
Provider-level tests could distinguish:
and verify that the explicit empty case used by report generation produces no callable built-in or MCP tools.
A report-phase integration test could additionally cover:
Related
This appears analogous to earlier provider capability-boundary work:
allowedTools: []This issue appears to be the Claude headless counterpart of the same general no-tools contract boundary.