Skip to content

Commit b02f42b

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's manifest schema rejects a string path, failing installation outright with `Validation errors: hooks: Invalid input`. Claude auto-discovers hooks/hooks.json, so the field is unnecessary — model-apps and power-pages both ship working hooks without it. 2. The `userPromptTransformed` key in hooks/hooks.json. Claude Code reads that file too and rejects unrecognized event keys. The resulting hook-load-failure silently discards the whole file, and also disables 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 is now split by host. Claude Code reads only .claude-plugin/plugin.json; Copilot CLI prefers .plugin/plugin.json and falls back to .claude-plugin/ only when absent. So the Copilot-only event lives inline in .plugin/plugin.json, where Claude Code never sees it: Claude Code | hooks/hooks.json (auto-discovered) | UserPromptSubmit Copilot CLI | .plugin/plugin.json (inline) | 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. 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. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8d478ed commit b02f42b

6 files changed

Lines changed: 123 additions & 21 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: 14 additions & 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",
@@ -9,6 +9,19 @@
99
"homepage": "https://github.com/microsoft/power-platform-skills/",
1010
"repository": "https://github.com/microsoft/power-platform-skills/",
1111
"license": "MIT",
12+
"hooks": {
13+
"userPromptTransformed": [
14+
{
15+
"hooks": [
16+
{
17+
"type": "command",
18+
"command": "dotnet run --file \"${PLUGIN_ROOT}/hooks/inject-sync-reminder.cs\"",
19+
"timeout": 30
20+
}
21+
]
22+
}
23+
]
24+
},
1225
"keywords": [
1326
"canvas-apps",
1427
"power-apps",

plugins/canvas-apps/AGENTS.md

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

2121
```
22-
.plugin/plugin.json ← Open Plugins metadata (name, version, keywords)
22+
.claude-plugin/plugin.json ← Claude Code metadata (Claude Code reads only this manifest)
23+
.plugin/plugin.json ← Open Plugins metadata + inline Copilot userPromptTransformed hook
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/
28+
hooks.json ← Claude Code UserPromptSubmit registration (Copilot hook is NOT here)
29+
inject-sync-reminder.cs ← File-based .NET app that emits the sync reminder for both hosts
2630
references/
2731
TechnicalGuide.md ← YAML syntax, control selection, layout strategies, Power Fx patterns
2832
DesignGuide.md ← Aesthetic guidelines, anti-patterns, design process
@@ -38,8 +42,6 @@ skills/
3842
SKILL.md ← Registers the Canvas Authoring MCP server with Claude Code
3943
add-data-source/
4044
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
4345
```
4446

4547
## Skills
@@ -75,6 +77,30 @@ The `canvas-authoring` MCP server exposes the following tools:
7577
| `list_data_sources` | Lists all available data sources in the current authoring session |
7678
| `sync_canvas` | Syncs the current coauthoring session state from the server to a local directory, writing all YAML files |
7779

80+
## Hooks
81+
82+
Both hosts inject a reminder to call `sync_canvas` before acting, so the agent never edits stale
83+
local `.pa.yaml` files. The reminder is conditional in wording — the agent skips the sync when no
84+
coauthoring session is active or the request is unrelated to a canvas app.
85+
86+
| Host | Registered in | Event |
87+
|------|---------------|-------|
88+
| Claude Code | `hooks/hooks.json` (auto-discovered) | `UserPromptSubmit` |
89+
| Copilot CLI | `.plugin/plugin.json` (inline `hooks`) | `userPromptTransformed` |
90+
91+
Both run `hooks/inject-sync-reminder.cs` via `dotnet run --file`, which branches on its stdin
92+
payload to emit the output shape each host expects.
93+
94+
Keep Copilot-specific events out of `hooks/hooks.json`: Claude Code rejects unrecognized hook
95+
event names and will fail to load the plugin, including its `canvas-authoring` MCP server.
96+
`.claude-plugin/plugin.json` must not declare a `"hooks"` field either — Claude auto-discovers
97+
`hooks/hooks.json`, and a path string there blocks installation.
98+
99+
Keep both manifests. Claude Code reads only `.claude-plugin/plugin.json`; Copilot reads
100+
`.plugin/plugin.json`. Deleting `.plugin/plugin.json` as a duplicate would make Copilot fall back
101+
to `.claude-plugin/`, which carries no Copilot event, and the reminder would stop reaching Copilot
102+
with no error.
103+
78104
## Prerequisites
79105

80106
Before the MCP server will start, you need:
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)