Skip to content

Commit 8aadf85

Browse files
rggammonclaude
andcommitted
Re-land canvas-apps sync reminder hook, fixed for Claude Code
Re-applies the hook from microsoft#320, which was reverted in microsoft#327 because it broke Claude Code. The hook logic (inject-sync-reminder.cs) is restored byte-for-byte; only the registration wiring changed. Two defects in microsoft#320, either one fatal: 1. `"hooks": "hooks/hooks.json"` in .claude-plugin/plugin.json. Claude Code rejects the path unless it is `./`-relative, failing installation with `Validation errors: hooks: Invalid input`. Even written as `"./hooks/hooks.json"` it is redundant — Claude auto-discovers the file, then reports the explicit reference as a duplicate hook source and disables the plugin's MCP servers. model-apps and power-pages ship working hooks with no `hooks` field at all. 2. `userPromptTransformed` in hooks/hooks.json. Claude Code reads that file and rejects unrecognized event names, silently discarding the whole file and disabling the plugin's canvas-authoring MCP server: [ERROR] Failed to load hooks for canvas-apps [DEBUG] Plugin not available for MCP - error type: hook-load-failed Registration now uses the two documented plugin hook formats, so each host reads only the events it supports: Claude Code | hooks/hooks.json (Claude format) | UserPromptSubmit Copilot CLI | hooks.json at plugin root (Copilot) | userPromptTransformed Two events are required because the hosts share no usable one: Copilot accepts UserPromptSubmit as an alias for userPromptSubmitted, but that event has no output processing, so the reminder would be generated and silently discarded. Both manifests stay byte-identical mirrors, so the legacy compatibility check is unaffected. Also re-applies microsoft#320's removal of the deprecated generate-canvas-app skill, which the revert restored, and corrects the AGENTS.md claim that Claude Code "ignores the Copilot-only event and continues loading UserPromptSubmit" — the behaviour that caused this outage. Verified on Claude Code 2.1.220 and Copilot CLI 1.0.76-1: plugin installs and loads (status enabled, MCP intact) and the reminder reaches the model in both hosts. All four repository validation scripts pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8d478ed commit 8aadf85

7 files changed

Lines changed: 128 additions & 20 deletions

File tree

plugins/canvas-apps/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "canvas-apps",
3-
"version": "2.2.1",
3+
"version": "2.2.2",
44
"description": "Build Power Apps Canvas Apps using the Canvas Authoring MCP server.",
55
"author": {
66
"name": "Microsoft",

plugins/canvas-apps/.plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "canvas-apps",
3-
"version": "2.2.1",
3+
"version": "2.2.2",
44
"description": "Build Power Apps Canvas Apps using the Canvas Authoring MCP server.",
55
"author": {
66
"name": "Microsoft",

plugins/canvas-apps/AGENTS.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ claude --plugin-dir /path/to/plugins/canvas-apps
1919
## Architecture
2020

2121
```
22+
.claude-plugin/plugin.json ← Claude Code metadata (mirrors .plugin/plugin.json)
2223
.plugin/plugin.json ← Open Plugins metadata (name, version, keywords)
2324
.mcp.json ← MCP server config (canvas-authoring, auto-registered)
2425
AGENTS.md ← Plugin guidance for AI agents (this file)
2526
CLAUDE.md ← Symlink → AGENTS.md
27+
hooks.json ← Copilot-format hooks: userPromptTransformed
28+
hooks/
29+
hooks.json ← Claude-format hooks: UserPromptSubmit
30+
inject-sync-reminder.cs ← File-based .NET app that emits the sync reminder for both hosts
2631
references/
2732
TechnicalGuide.md ← YAML syntax, control selection, layout strategies, Power Fx patterns
2833
DesignGuide.md ← Aesthetic guidelines, anti-patterns, design process
@@ -38,8 +43,6 @@ skills/
3843
SKILL.md ← Registers the Canvas Authoring MCP server with Claude Code
3944
add-data-source/
4045
SKILL.md ← Guides user to add a data source or connector in Studio, then verifies
41-
generate-canvas-app/
42-
SKILL.md ← [DEPRECATED] Redirects to canvas-app
4346
```
4447

4548
## Skills
@@ -75,6 +78,31 @@ The `canvas-authoring` MCP server exposes the following tools:
7578
| `list_data_sources` | Lists all available data sources in the current authoring session |
7679
| `sync_canvas` | Syncs the current coauthoring session state from the server to a local directory, writing all YAML files |
7780

81+
## Hooks
82+
83+
Both hosts inject a reminder to call `sync_canvas` before acting, so the agent never edits stale
84+
local `.pa.yaml` files. The reminder is conditional in wording — the agent skips the sync when no
85+
coauthoring session is active or the request is unrelated to a canvas app.
86+
87+
Registration uses both plugin [hook formats](https://code.visualstudio.com/docs/agent-customization/agent-plugins)
88+
— Claude-format (`hooks/hooks.json`) and Copilot-format (`hooks.json` at the plugin root) — which
89+
the hosts read independently:
90+
91+
| Host | Registered in | Event |
92+
|------|---------------|-------|
93+
| Claude Code | `hooks/hooks.json` (Claude format) | `UserPromptSubmit` |
94+
| Copilot CLI | `hooks.json` in the plugin root (Copilot format) | `userPromptTransformed` |
95+
96+
Each host reads only its own file and ignores the other's. Both run
97+
`hooks/inject-sync-reminder.cs` via `dotnet run --file`, which branches on its stdin payload to
98+
emit the output shape each host expects.
99+
100+
Keep the two files separate. Claude Code rejects unrecognized hook event names, so putting
101+
`userPromptTransformed` in `hooks/hooks.json` makes Claude fail to load the plugin, disabling its
102+
`canvas-authoring` MCP server. Neither manifest should declare a `"hooks"` field: Claude Code
103+
auto-discovers `hooks/hooks.json`, and pointing at it explicitly is treated as a duplicate hook
104+
source, which also disables MCP.
105+
78106
## Prerequisites
79107

80108
Before the MCP server will start, you need:

plugins/canvas-apps/hooks.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"version": 1,
3+
"description": "Canvas Apps lifecycle hooks (Copilot format)",
4+
"hooks": {
5+
"userPromptTransformed": [
6+
{
7+
"hooks": [
8+
{
9+
"type": "command",
10+
"command": "dotnet run --file \"${PLUGIN_ROOT}/hooks/inject-sync-reminder.cs\"",
11+
"timeout": 30
12+
}
13+
]
14+
}
15+
]
16+
}
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"description": "Canvas Apps lifecycle hooks",
3+
"hooks": {
4+
"UserPromptSubmit": [
5+
{
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "dotnet run --file \"${CLAUDE_PLUGIN_ROOT}/hooks/inject-sync-reminder.cs\"",
10+
"timeout": 30
11+
}
12+
]
13+
}
14+
]
15+
}
16+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#:property PublishAot=false
2+
3+
using System.Text.Json;
4+
5+
// Prompt hook for the canvas-apps plugin.
6+
//
7+
// The hook injects guidance instead of calling sync_canvas directly because it only
8+
// receives the session cwd, not the per-app working directory. A blanket sync could
9+
// overwrite the wrong directory or clobber local edits that have not been pushed.
10+
const string Reminder =
11+
"Canvas Apps coauthoring reminder: " +
12+
"Before acting on this request, if a Canvas Authoring session is " +
13+
"active (i.e. `connect` has already been called for a canvas app), first call " +
14+
"the `sync_canvas` MCP tool targeting the app's working directory to pull the " +
15+
"current session state into the local .pa.yaml files. This avoids editing stale " +
16+
"YAML. If no coauthoring session is active yet, or this request is unrelated to " +
17+
"a canvas app, skip the sync.";
18+
19+
try
20+
{
21+
var rawInput = await Console.In.ReadToEndAsync();
22+
string? transformedPrompt = null;
23+
24+
try
25+
{
26+
using var input = JsonDocument.Parse(rawInput);
27+
if (input.RootElement.ValueKind == JsonValueKind.Object &&
28+
input.RootElement.TryGetProperty("transformedPrompt", out var prompt) &&
29+
prompt.ValueKind == JsonValueKind.String)
30+
{
31+
transformedPrompt = prompt.GetString();
32+
}
33+
}
34+
catch (JsonException)
35+
{
36+
// Hook failures must not block the user's prompt. If parsing fails, emit the
37+
// Claude Code additional-context shape (Copilot will ignore it).
38+
}
39+
40+
using var writer = new Utf8JsonWriter(Console.OpenStandardOutput());
41+
writer.WriteStartObject();
42+
43+
if (transformedPrompt is not null)
44+
{
45+
writer.WriteString(
46+
"modifiedTransformedPrompt",
47+
$"{transformedPrompt}\n\n{Reminder}");
48+
}
49+
else
50+
{
51+
writer.WriteStartObject("hookSpecificOutput");
52+
writer.WriteString("hookEventName", "UserPromptSubmit");
53+
writer.WriteString("additionalContext", Reminder);
54+
writer.WriteEndObject();
55+
}
56+
57+
writer.WriteEndObject();
58+
writer.Flush();
59+
}
60+
catch
61+
{
62+
// Hooks are advisory and must never prevent the host from processing a prompt.
63+
}

plugins/canvas-apps/skills/generate-canvas-app/SKILL.md

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)