The Plan Canvas library wrote ~/.claude/plan-canvas while the SessionStart
and Stop hooks read CLAUDE_CONFIG_DIR/plan-canvas. Under a second profile
SessionStart missed open reviews and Stop let a turn end with feedback
still queued.
Expand a leading ~ in CLAUDE_CONFIG_DIR the way scripts/lib/agent-data-home.js
does, so a profile written as ~/.claude-work resolves to one directory rather
than two.
Add hook-level coverage that spawns each hook with a distinct
CLAUDE_CONFIG_DIR and no ECC_* override, plus a producer-to-consumer round
trip. Eight of the nine cases fail against the previous revision.
Claude Code points every profile-local path at CLAUDE_CONFIG_DIR when it is
set, but ECC hooks kept reading and writing ~/.claude. A session launched
under a second profile left its bash log, cost tracker, metrics, session data
and skill runs in the default profile, where the other one reads them back.
Two groups, as reported:
- getDefaultClaudeAgentDataHome() in scripts/lib/agent-data-home.js now
follows the variable, which covers every hook reaching ~/.claude through
resolveAgentDataHome(). ECC_AGENT_DATA_HOME still wins, so the documented
workaround keeps working.
- plan-canvas-pending, plan-canvas-sessions, mcp-health-check and
post-bash-command-log hard-coded os.homedir()/.claude and now resolve the
same way, matching scripts/hooks/ecc-statusline.js:68.
A blank value is ignored rather than resolved, since path.resolve(' ')
returns the process cwd and would scatter profile data through whatever
directory the session started in.
Fixes#3266
Feedback sent from the canvas only reached an agent through a live
/api/await long poll. When a turn ended with no await parked,
queueFeedback wrote the message to sessions.json and nothing ever
consumed it, so sending appeared to do nothing at all. The presence pill
made it worse: workingKeys had no expiry and the feedback handler never
broadcast presence, so it froze on "agent working" while nobody was
listening.
Delivery:
- Add the stop:plan-canvas-pending hook. It drains undelivered feedback
and blocks the Stop, handing the messages to the agent, so a canvas
message lands even when no await is running. Scoped to sessions under
cwd so parallel agents cannot swallow each other's feedback; set
ECC_PLAN_CANVAS_STOP_SCOPE=all to widen. Honors stop_hook_active and
fails open on every error path.
- run-with-flags.js did not await a hook's run(), so any async hook
silently degraded to pass-through. Fixed; plan-canvas-pending is the
only async hook today.
Presence and indicators:
- Presence is now ended/typing/thinking/listening/queued/waiting.
thinking and typing self-expire (90s/30s) and a 5s sweep pushes the
decay to an idle browser, so the pill can no longer stick.
- Broadcast presence when feedback is queued, and clear the activity
state when an agent reply lands.
- Add POST /api/session/:key/typing so agents can drive the indicator.
- Chat shows an animated dots bubble for thinking and typing, plus an
explicit note when a message is queued with nobody listening.
Respects prefers-reduced-motion.
- Send status reports what actually happened instead of always claiming
the agent will pick it up.
CLI and skill:
- Add `ecc-plan-canvas pending` and `typing <file> --state ...`.
- SKILL.md documents background await as the primary pattern and makes
replying in the canvas mandatory.
Tests: 6 new server cases covering queued presence, the typing endpoint,
state expiry and the sweep, plus a new hook suite covering delivery,
drain-once, stop_hook_active, cwd scoping and fail-open.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>