forked from admin/VolcanicAgenticCore
fix(claude-code): disable built-in plugins and learn new ones from init
Claude Code 2.1.287 loads cc-plugin-* built-ins regardless of --setting-sources, which tripped the fail-closed init check. Disable every known built-in in --settings, and when init lists only built-ins that slipped through, disable them process-wide and replay the request once. Other plugins still fail closed.
This commit is contained in:
@@ -20,7 +20,7 @@ public class ClaudeArgsTests
|
||||
[
|
||||
"-p",
|
||||
"--setting-sources", "",
|
||||
"--settings", "{\"disableAllHooks\":true,\"autoMemoryEnabled\":false,\"totalTokensReminder\":\"off\",\"enabledPlugins\":{\"agents-md@builtin\":false}}",
|
||||
"--settings", ClaudeArgs.Settings,
|
||||
"--disable-slash-commands",
|
||||
"--strict-mcp-config",
|
||||
"--permission-mode", "dontAsk",
|
||||
@@ -66,6 +66,29 @@ public class ClaudeArgsTests
|
||||
Assert.DoesNotContain("--effort", args);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Settings_disable_hooks_memory_and_every_known_builtin_plugin()
|
||||
{
|
||||
var settings = JsonSerializer.Deserialize<JsonElement>(ClaudeArgs.Settings);
|
||||
|
||||
Assert.True(settings.GetProperty("disableAllHooks").GetBoolean());
|
||||
Assert.False(settings.GetProperty("autoMemoryEnabled").GetBoolean());
|
||||
Assert.Equal("off", settings.GetProperty("totalTokensReminder").GetString());
|
||||
var plugins = settings.GetProperty("enabledPlugins").EnumerateObject().ToList();
|
||||
Assert.Equal(BuiltinPlugins.Known.Order(StringComparer.Ordinal), plugins.Select(p => p.Name).Order(StringComparer.Ordinal));
|
||||
Assert.All(plugins, p => Assert.Equal(JsonValueKind.False, p.Value.ValueKind));
|
||||
Assert.Contains("cc-plugin-plugin-authoring@builtin", plugins.Select(p => p.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_disables_the_given_plugins()
|
||||
{
|
||||
var args = ClaudeArgs.Build(new ClaudeArgsInput("haiku", null, "/tmp/p/system-prompt.txt", DisabledPlugins: ["x@builtin"]));
|
||||
|
||||
var settings = JsonSerializer.Deserialize<JsonElement>(args[args.ToList().IndexOf("--settings") + 1]);
|
||||
Assert.Equal(["x@builtin"], settings.GetProperty("enabledPlugins").EnumerateObject().Select(p => p.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mcp_config_honours_the_server_name_and_requires_bridge_and_dotnet()
|
||||
{
|
||||
|
||||
@@ -47,8 +47,11 @@ public class ClaudeCodeApiTests : IDisposable
|
||||
return script;
|
||||
}
|
||||
|
||||
private readonly BuiltinPlugins _builtins = new();
|
||||
|
||||
private ClaudeCodeApi Api(string claudePath, Func<CancellationToken, Task<IUserInterface?>>? loginUi = null, ClaudeLoginCoordinator? coordinator = null) => new(_registry, new ClaudeCodeApiOptions
|
||||
{
|
||||
BuiltinPlugins = _builtins,
|
||||
LoginUi = loginUi,
|
||||
Coordinator = coordinator ?? new ClaudeLoginCoordinator(),
|
||||
ClaudePath = claudePath,
|
||||
@@ -571,4 +574,69 @@ public class ClaudeCodeApiTests : IDisposable
|
||||
Assert.Equal([0], responses);
|
||||
Assert.Equal(0, _loginRequests);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A fake claude that loads <paramref name="plugin"/> unless <c>--settings</c> disables it (or always, with
|
||||
/// <paramref name="ignoreSettings"/>); without the plugin it proposes the probe tool.
|
||||
/// </summary>
|
||||
private string PluginClaude(string plugin, bool ignoreSettings = false)
|
||||
{
|
||||
var state = Path.Combine(_root, "state");
|
||||
Directory.CreateDirectory(state);
|
||||
var init = Fixtures.ProbeInit().GetRawText().Replace("\"plugins\":[]", $"\"plugins\":[{plugin}]", StringComparison.Ordinal);
|
||||
return FakeClaude(
|
||||
$"echo run >> \"{state}/runs\"\n" +
|
||||
"settings=\"\"\nprev=\"\"\n" +
|
||||
"for a in \"$@\"; do if [ \"$prev\" = \"--settings\" ]; then settings=\"$a\"; fi; prev=\"$a\"; done\n" +
|
||||
(ignoreSettings ? "" : "case \"$settings\" in *'\"cc-plugin-new@builtin\":false'*) " +
|
||||
$"cat \"{Fixtures.FixturePath("claude-2.1.281-stream-tool-ok.jsonl")}\"; exit 0;; esac\n") +
|
||||
$"echo '{init}'",
|
||||
tail: "exit 0");
|
||||
}
|
||||
|
||||
private const string NewBuiltin = "{\"name\":\"cc-plugin-new\",\"path\":\"builtin\",\"source\":\"cc-plugin-new@builtin\"}";
|
||||
|
||||
[Fact]
|
||||
public async Task An_unexpected_builtin_plugin_is_disabled_and_the_request_replayed_once()
|
||||
{
|
||||
if (!Fixtures.IsPosix) return;
|
||||
var api = Api(PluginClaude(NewBuiltin));
|
||||
|
||||
var events = await Collect(api.StreamAsync(Fixtures.Sonnet, ToolContext(), new StreamOptions { SessionId = "session-1" }, CancellationToken.None));
|
||||
|
||||
Assert.Equal(["Start", "TextStart", "TextDelta", "TextEnd", "ToolCallStart", "ToolCallDelta", "ToolCallEnd", "Done"], events.Select(e => e.GetType().Name).ToArray());
|
||||
Assert.Equal(2, Runs());
|
||||
Assert.Contains("cc-plugin-new@builtin", _builtins.Disabled);
|
||||
|
||||
// Learned for the process: the next request disables it from the start.
|
||||
await Collect(api.StreamAsync(Fixtures.Sonnet, ToolContext(), new StreamOptions { SessionId = "session-1" }, CancellationToken.None));
|
||||
Assert.Equal(3, Runs());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_builtin_plugin_that_stays_loaded_fails_after_one_replay()
|
||||
{
|
||||
if (!Fixtures.IsPosix) return;
|
||||
var api = Api(PluginClaude(NewBuiltin, ignoreSettings: true));
|
||||
|
||||
var events = await Collect(api.StreamAsync(Fixtures.Sonnet, ToolContext(), new StreamOptions { SessionId = "session-1" }, CancellationToken.None));
|
||||
|
||||
var error = Assert.IsType<AssistantMessageEvent.Error>(Assert.Single(events));
|
||||
Assert.Contains("[protocol_init] Claude Code loaded unexpected customizations (plugins: cc-plugin-new)", error.Message.ErrorMessage);
|
||||
Assert.Equal(2, Runs());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_non_builtin_plugin_fails_closed_without_a_replay()
|
||||
{
|
||||
if (!Fixtures.IsPosix) return;
|
||||
var api = Api(PluginClaude("{\"name\":\"cc-plugin-new\",\"path\":\"/home/u/.claude/plugins/x\",\"source\":\"cc-plugin-new@market\"}"));
|
||||
|
||||
var events = await Collect(api.StreamAsync(Fixtures.Sonnet, ToolContext(), new StreamOptions { SessionId = "session-1" }, CancellationToken.None));
|
||||
|
||||
var error = Assert.IsType<AssistantMessageEvent.Error>(Assert.Single(events));
|
||||
Assert.Contains("unexpected customizations", error.Message.ErrorMessage);
|
||||
Assert.Equal(1, Runs());
|
||||
Assert.DoesNotContain("cc-plugin-new@market", _builtins.Disabled);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user