Fix artifact extraction for create_file MCP tool (v1.10.15)

When Anthropic's `enabled_artifacts_attachments` conversation setting
is false, Claude creates files via the skills-runner `create_file` MCP
tool instead of the legacy `artifacts` tool. The display_content shape
is identical (json_block with language / code / filename), only the
tool_use.name differs. The v1.9.1 strict allowlist was rejecting it,
silently dropping every artifact in those conversations.

Allowlist now accepts both 'artifacts' and 'create_file'. Other skills
tools that happen to share json_block display (view, list_directory,
etc.) stay filtered out by name.

Two regression tests added:
- positive: create_file with markdown json_block extracts correctly
- negative: view tool with similar json_block stays filtered

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
tux tucker 2026-05-18 22:06:43 -04:00
parent bec92d687f
commit f125602d12
3 changed files with 75 additions and 6 deletions

View file

@ -247,9 +247,17 @@ function extractArtifactsFromMessage(message) {
// Check if message has content array (new format)
if (message.content && Array.isArray(message.content)) {
for (const content of message.content) {
// NEW FORMAT: tool_use with display_content
// Only the `artifacts` tool produces real artifacts — bash, web_search, repl, etc. are filtered out
if (content.type === 'tool_use' && content.name === 'artifacts' && content.display_content) {
// NEW FORMAT: tool_use with display_content.
// Allowlist real file/artifact producers:
// - `artifacts` — legacy artifacts tool (still used when
// `enabled_artifacts_attachments` is true)
// - `create_file` — skills-runner MCP tool that replaced artifacts
// when `enabled_artifacts_attachments` is false. Same json_block
// display_content shape (language / code / filename).
// bash, web_search, repl, view, list_directory, etc. are filtered out.
if (content.type === 'tool_use' &&
(content.name === 'artifacts' || content.name === 'create_file') &&
content.display_content) {
const displayContent = content.display_content;
// Check for code_block format (newer artifact format)

View file

@ -247,9 +247,17 @@ function extractArtifactsFromMessage(message) {
// Check if message has content array (new format)
if (message.content && Array.isArray(message.content)) {
for (const content of message.content) {
// NEW FORMAT: tool_use with display_content
// Only the `artifacts` tool produces real artifacts — bash, web_search, repl, etc. are filtered out
if (content.type === 'tool_use' && content.name === 'artifacts' && content.display_content) {
// NEW FORMAT: tool_use with display_content.
// Allowlist real file/artifact producers:
// - `artifacts` — legacy artifacts tool (still used when
// `enabled_artifacts_attachments` is true)
// - `create_file` — skills-runner MCP tool that replaced artifacts
// when `enabled_artifacts_attachments` is false. Same json_block
// display_content shape (language / code / filename).
// bash, web_search, repl, view, list_directory, etc. are filtered out.
if (content.type === 'tool_use' &&
(content.name === 'artifacts' || content.name === 'create_file') &&
content.display_content) {
const displayContent = content.display_content;
// Check for code_block format (newer artifact format)

View file

@ -114,6 +114,59 @@ describe('extractArtifactsFromMessage — tool name filter', () => {
};
expect(extractArtifactsFromMessage(message)).toEqual([]);
});
// Regression: when `enabled_artifacts_attachments` is false in conversation
// settings, Claude uses the skills-runner `create_file` MCP tool instead of
// the legacy `artifacts` tool. display_content shape is identical
// (json_block with language / code / filename). The extractor must allowlist
// both tool names.
it('extracts a create_file tool_use (skills-runner replacement for artifacts)', () => {
const message = {
content: [
{
type: 'tool_use',
name: 'create_file',
input: {
path: '/mnt/user-data/outputs/hello.md',
file_text: '# Hello, world!\n',
},
display_content: {
type: 'json_block',
json_block: JSON.stringify({
language: 'markdown',
code: '# Hello, world!\n',
filename: '/mnt/user-data/outputs/hello.md',
}),
},
},
],
};
const artifacts = extractArtifactsFromMessage(message);
expect(artifacts).toHaveLength(1);
expect(artifacts[0].title).toBe('hello');
expect(artifacts[0].language).toBe('markdown');
expect(artifacts[0].content).toBe('# Hello, world!');
});
it('still rejects other skills tools that share json_block display (e.g. view, list_directory)', () => {
const message = {
content: [
{
type: 'tool_use',
name: 'view',
display_content: {
type: 'json_block',
json_block: JSON.stringify({
language: 'text',
code: 'directory listing here',
filename: '/mnt/skills/public',
}),
},
},
],
};
expect(extractArtifactsFromMessage(message)).toEqual([]);
});
});
describe('extractArtifactFiles — end-to-end', () => {