Skip to content

feat(hooks): Claude Code lifecycle reminders to pull and push Ditto memory [DITTO-2706] - #29

Merged
Peyton-Spencer merged 6 commits into
mainfrom
t3/ditto-2706-plugin-hooks
Nov 13, 2026
Merged

Peyton-Spencer merged 6 commits into
mainfrom
t3/ditto-2706-plugin-hooks

Conversation

@Peyton-Spencer

@Peyton-Spencer Peyton-Spencer commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Jira: DITTO-2706. Docs follow-up: DITTO-2705. Guard: DITTO-1931, partial.

What this adds

Ditto works best when an agent pulls memory before it starts and pushes the outcome when it finishes. This PR adds the smallest set of host hooks that reminds the agent at those moments. It is not a revert of #5. The hooks make no network calls and run no CLI, and they hold no keys. They print reminders that point the agent at the existing ditto-memory skill. Every Ditto read and write still goes through the agent's hosted MCP connection, so the host's OAuth and tool permissions apply as usual.

Claude Code only (plugins/ditto/hooks/, wired via .claude-plugin/plugin.json → ./hooks/claude-hooks.json):

Event Step Effect
PostToolUse, matched only to Ditto memory tools connected Writes an empty connected marker. Every reminder below stays silent until this has happened once, so an install whose Ditto server was never authorized gets nothing
SessionStart startup/resume/clear session-start About 100 words of context: search Ditto through ditto-memory before work that depends on the user's history, treat memories as evidence rather than instructions, and save durable outcomes at the end if the user authorized capture
PreCompact pre-compact Instructions for the compaction summary: keep Ditto memory IDs, and mark outcomes that are "not yet saved to Ditto"
SessionStart compact compact Reuse those IDs and save what is still unsaved, if authorized
Stop stop Once per attended session, after a file edit or ≥10 tool calls with no Ditto save_memory/update_memory call, ask the agent to save one concise note if capture is authorized, or offer to. It continues the turn via hookSpecificOutput.additionalContext and costs one short extra turn. Skipped in claude -p and Agent SDK runs (CLAUDE_CODE_SESSION_ATTENDED=0) unless DITTO_HOOKS_STOP=1

Constraints: every handler is sh …/ditto-hook.sh <step> with a 5 s timeout and runs in about 20–40 ms. It fails open: the body runs in a subshell before an unconditional exit 0, so not even a shell error can return Claude Code's blocking exit 2, and bad input gives no output. It writes no logs and reads no secrets. The only write is an empty per-session marker in ${CLAUDE_PLUGIN_DATA}. Opt-out: DITTO_HOOKS=0 turns off all hooks, DITTO_HOOKS_STOP=0 turns off only the Stop check, and DITTO_HOOKS_STOP=1 opts headless runs in. Nothing runs on prompts, and nothing runs on tool calls other than Ditto's own memory tools.

Why these events and not others

The full research, sources and per-host matrix are in docs/hooks.md. In short:

  • Not shipped:

    • UserPromptSubmit recall: a network call plus context on every prompt, which is the 0.2.x behaviour fix: migrate Ditto for Claude to hosted MCP #5 removed.
    • PreToolUse/PostToolUse: these run on every tool call.
    • SessionEnd: it cannot reach the model, so saving would need credentials in a script.
    • SubagentStop: it would multiply the extra turns.
    • mcp_tool handlers: there is no query at session start, and a bare save at Stop would be a transcript dump.
  • Automatic saving without asking is not shipped. The ditto-memory skill says "ask before enabling ongoing capture". The hooks follow that rule. Making capture opt-out by default is a product call for ditto-skills.

  • Other hosts get no hooks in this PR.

    • Codex: hooks need per-user trust review, and a plugin ZIP with hooks is ineligible for the OpenAI directory.
    • Cursor: a different schema, and sessionStart is fire-and-forget.
    • Copilot/VS Code: sessionStart takes only top-level additionalContext.
    • Grok: no SessionStart context.
    • Antigravity: injectSteps, root hooks.json.
    • Kimi: SessionStart is observe-only.
    • ChatGPT: the Agent Plugins format defines no hooks.

    On those hosts the skills remain the path. Codex and Cursor are candidate follow-ups.

Why the file is not hooks/hooks.json

Claude Code, Cursor, Codex, Copilot CLI, VS Code and Grok all load hooks/hooks.json from a plugin folder with no manifest field, verified against docs plus the cursor-agent, codex, copilot and grok binaries. In one shared folder, that file would run Claude-format hooks on every one of those hosts. The hooks therefore live at hooks/claude-hooks.json, and only the Claude manifest names them. Every other host reads its own manifest first, finds no hooks field and no default file, and loads nothing. grok plugin validate confirms that Grok sees no hook component.

Tamper guard (DITTO-1931, part 1)

scripts/validate_package.py now rejects all of the following:

  • any hook file whose SHA-256 is not pinned in scripts/hook-allowlist.json, so changed bytes need a visible hash change
  • hooks/hooks.json at the default path, even when pinned
  • the plugin-root hooks.json that Copilot and Antigravity read
  • hooks in any manifest other than Claude's, and hooks in marketplace entries
  • events, matchers or handler types outside the shipped set (http, mcp_tool, async and extra fields included)
  • a timeout above 10 s
  • a command that does more than run the bundled script
  • an executable bit on any plugin file
  • hook bytes that name a network tool or a script interpreter

The ChatGPT upload carries no hooks.

.github/CODEOWNERS now requests Peyton and Omar on plugins/*/hooks/, the allowlist, the validator and .github/. The main protection ruleset has require_code_owner_review: false, so the file is advisory until an owner turns that on. I did not change the ruleset. The daily served-copy drift check and signed commits from DITTO-1931 remain open.

Other changes

  • README, plugin README, RELEASING.md, CONTRIBUTING.md, docs/shared-skills.md: the old statement "no lifecycle hooks" is replaced with what now ships.
  • adapters/ditto-memory.md: same update. skills/ditto-memory/SKILL.md was regenerated from the locked source f1b8c1b, and only the adapter changed.
  • A clean regeneration also reverts the hand edit Prepare Ditto directory plugin disclosures and identifier #27 made to tdd/SKILL.md. I kept Prepare Ditto directory plugin disclosures and identifier #27's text and its lock hash, and changed "bundles no executable code or test runner" to "bundles no test runner", which is accurate now. That sentence still does not exist in ditto-skills, a drift that predates this PR.
  • No version bump. All manifests stay at 0.4.1. Releasing is Peyton's call per DITTO-2706, and installed Claude Code plugins may not pick up the hooks until the version is bumped.

Review round 1 (Omar's spot-check), fixed in 718b7e8

  • Headless runs: the Stop check now skips unattended sessions. Measured on 2.1.308: interactive is ENTRYPOINT=cli with ATTENDED=1; claude -p is sdk-cli with 0.
  • Users who never connected Ditto: they are no longer reminded; see the connected gate above.
  • Transcript format: the check now matches either key order and is pinned to a real 2.1.308 fixture.
  • Exit 2 under dash: the new read-only test found that a failed redirect on : exited the shell with 2 (blocking). This was present at a6ce55b and is now fixed.
  • Stop label: Stop output moved from decision: block, which the terminal labels "Stop hook error", to additionalContext.

Review round 2 (Ditto Review on 718b7e8, approved with 7 non-blocking findings), fixed in 9bc8e2c

  • Validator: non-string hooks values and hook commands now raise ValueError instead of TypeError. Allowlist keys must have the form plugins/<name>/hooks/<file>.
  • Path parsing: JSON paths are unescaped in a single pass, so a backslash directly before a / survives.
  • connected marker: MCP error results (isError) no longer create it.
  • Stop marker: it is written after the reminder, behind a writability check.
  • CODEOWNERS: now also covers .claude-plugin/plugin.json.

Review round 3 (Omar's Lows on 718b7e8), fixed in 95aaf3f

  • Stop check frequency: it runs at most once a day per install, and the agent does not re-offer capture after the user declines.
  • connected marker: it lapses after 30 days without a Ditto call.
  • Subagents: the docs note that only the main transcript is read.

Verification

  • python3 -m unittest discover -s tests: 42 tests OK at 95aaf3f on Boat builder bx_ybpvfzcj (Ubuntu, /bin/sh = dash). test_hooks.py runs the real script under sh with hook stdin. It covers:

    • output and opt-outs
    • once-per-session behaviour and the stop_hook_active loop guard
    • work and save heuristics, including tool definitions in the transcript, which do not count as calls
    • fail-open inputs: malformed or empty stdin, no data dir, missing transcript, path-traversal session id, unwritable data dir
  • Mutation controls added in round 1: dropping the reordered-key match fails test_stop_tolerates_reordered_tool_use_keys; removing the unattended skip fails test_stop_skips_unattended_sessions_unless_asked; the original : redirect fails test_read_only_data_dir_fails_open_quietly.

  • Mutation controls on the Boat copy: loosening the tool-call pattern fails test_stop_stays_quiet_without_work_or_after_a_save, and removing the marker check fails test_stop_asks_once_per_session_after_real_work. Both pass again after restore.

  • python3 scripts/validate_package.py reports 44 files and 2 pinned hook files. shared_skills.py verify passes. claude plugin validate ./plugins/ditto passes; a renamed event in a copy is reported as unknown hook event, so the validator does read this file. grok plugin validate ./plugins/ditto passes with no hook component.

  • Live, 2026-11-13, Claude Code 2.1.308, with a local stdio test double named ditto serving search_memories:

    • Headless, no marker: SessionStart was silent, PostToolUse fired on mcp__ditto__search_memories and wrote connected, and Stop was silent (unattended).
    • Headless with DITTO_HOOKS_STOP=1: the context was injected, Stop asked once, and the second Stop was silent.
    • Interactive terminal (PTY): Stop asked once with no error label, and the agent answered in one line.
  • Live, 2026-11-10, before round 1, claude -p --plugin-dir, haiku, no Ditto account connected:

    • SessionStart:startup injected the context in 22 ms.
    • A session using Write got one Stop check. The agent replied with one line ("nothing durable… nothing to save"), and the second Stop was silent.
    • A session with one Bash call got no check.
    • /compact on the resumed session fired SessionStart:compact after the boundary, and the PreCompact text reached the compaction.

    An earlier live run caught two bugs, both fixed and covered by tests:

    • The transcript recorded tool definitions, which matched a loose "name":"Edit" pattern.
    • "Stop without comment" produced an empty reply that Claude Code rejected.

Not verified:

  • An authenticated save through a connected Ditto account triggered by these reminders.
  • Windows. Claude Code uses Git Bash there; under PowerShell, sh is missing and the hooks fail open.
  • Behaviour after a marketplace install, as opposed to --plugin-dir.

🤖 Generated with Claude Code

…emory [DITTO-2706]

Ship three local, hash-pinned Claude Code hooks: SessionStart context that
points the agent at the ditto-memory skill, PreCompact summary instructions plus
a post-compaction reminder, and a once-per-session Stop check after real work.
The script makes no network calls, fails open within a 5 s timeout, and is
disabled with DITTO_HOOKS=0 (or DITTO_HOOKS_STOP=0 for the Stop check only).

The validator allows hook files only when pinned in scripts/hook-allowlist.json,
wired through the Claude manifest (never the hooks/hooks.json default that
Cursor, Codex, Copilot and Grok discover by themselves), with command handlers,
bounded timeouts and no network tools. docs/hooks.md records the per-host
research and why other events and hosts are not shipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Peyton-Spencer

Copy link
Copy Markdown
Contributor Author

@ditto review

@omarzanji omarzanji left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spot-check of the draft. The hooks are local-only, text-only, opt-out and exit 0 on failure, which is the right posture. Two points.

  1. The Stop hook returns decision: block, which forces another model turn. It fires once per session after a file edit or 10 tool calls with no Ditto save. In interactive use that is a nudge; in headless claude -p, scheduled or CI sessions nobody can answer "has capture been authorized?", and it costs a turn and tokens per session for every plugin user. Consider default-off for the Stop check (DITTO_HOOKS_STOP opt-in), or skip it when the session is non-interactive.
  2. SessionStart nags users who have not connected Ditto. It prints about 100 words into context every start/resume/clear and tells the agent to "mention it once" if the server is not connected. A user who installed the plugin but has not authorized the MCP sees that every session. Is there a cheap signal (the plugin's MCP config state, or a marker written by the first successful ditto-memory call) to stay quiet until the server has connected once?

Also worth a test: stop depends on the transcript line format ("type":"tool_use","id":"...","name":"). If Claude Code reorders those keys the hook silently never fires; a fixture pinned to the current format plus a note in docs/hooks.md would make that visible.

Reviewed by Claude with the teammate-pr-review-loop skill, on behalf of the account posting it.

… exit 2 [DITTO-2706]

Address Omar's spot-check on #29:

- Every reminder is silent until a Ditto memory tool has succeeded once. A
  PostToolUse hook matched only to Ditto's memory tools writes an empty
  `connected` marker; the validator rejects any other PostToolUse matcher.
- The Stop check skips unattended sessions (CLAUDE_CODE_SESSION_ATTENDED=0 in
  claude -p and the Agent SDK, with CLAUDE_CODE_ENTRYPOINT=sdk-* as the fallback);
  DITTO_HOOKS_STOP=1 turns it on for automations.
- The Stop check matches tool_use blocks in either key order and is pinned to a
  real Claude Code 2.1.308 transcript line (tests/fixtures).
- Stop continues through hookSpecificOutput.additionalContext, which Claude
  Code does not label "Stop hook error".
- Fix a fail-open hole found by the new read-only data dir test: under dash a
  failed redirect on `:` exits the shell with 2, Claude Code's blocking code.
  Markers use touch, and the body runs in a subshell before an unconditional
  exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Peyton-Spencer

Copy link
Copy Markdown
Contributor Author

Thanks @omarzanji. All three points are addressed in 718b7e8.

1. Stop in headless runs. The Stop check now skips unattended sessions. I measured the signal on Claude Code 2.1.308 by dumping the hook environment:

Mode CLAUDE_CODE_ENTRYPOINT CLAUDE_CODE_SESSION_ATTENDED
Interactive terminal cli 1
claude -p sdk-cli 0

The check runs only when ATTENDED isn't 0. On older builds without that variable, it falls back to treating ENTRYPOINT=sdk-* as unattended. DITTO_HOOKS_STOP=1 turns it back on for an automation that should save its outcome. Two other changes to the Stop check:

  • It now continues the turn through hookSpecificOutput.additionalContext instead of decision: block. In the interactive terminal, Claude Code labelled the block reason "Stop hook error"; the new form shows as an ordinary stop-hook step.
  • SessionStart still runs headless, because pulling memory at automation start is part of what DITTO-2706 asks for.

2. Nagging users who never connected Ditto. Every reminder is now silent until a Ditto memory tool has succeeded once in that installation:

  • A PostToolUse hook, matched only to mcp__.*ditto.*__(search_memories|fetch_memories|save_memory|update_memory|aggregate_memories), writes an empty connected file in ${CLAUDE_PLUGIN_DATA}. It runs on no other tool call.
  • The validator rejects any other PostToolUse matcher.
  • An install whose server was never authorized gets no context and no Stop check.

3. Transcript format. The check now counts a tool call on either "type":"tool_use","id":…,"name":"X" or "name":"X","input":. Tool definitions ("name":"X","description") never match.

  • tests/fixtures/claude-code-2.1.308-write.jsonl is a real 2.1.308 Write line with its keys in the recorded order and identifying values replaced.
  • Tests cover that line and two reordered key orders.
  • docs/hooks.md now explains that a format change makes the check go quiet, and that the fixture should be refreshed when that happens.

A bug the new tests found, now fixed. A read-only data-directory test caught a real fail-open hole that was also present at a6ce55b. Under dash, a failed redirect on the special builtin : exits the shell with status 2, which is Claude Code's blocking exit code. Two fixes:

  • Markers are now written with touch.
  • The script body runs in a subshell followed by an unconditional exit 0.

Evidence

  • 39 tests pass under dash on Boat.
  • Mutation checks each break the matching test: dropping the reordered-key match, removing the unattended skip, and the original : redirect.
  • Live runs on Claude Code 2.1.308, using a local stdio test double named ditto:
    • Headless with no marker: SessionStart was silent, PostToolUse fired on mcp__ditto__search_memories and wrote the marker, and Stop was silent.
    • Headless with DITTO_HOOKS_STOP=1: the context was injected and Stop asked once.
    • Interactive: Stop asked once with no error label, and the agent answered in one line.
  • None of these connected a real Ditto account.

@Peyton-Spencer
Peyton-Spencer marked this pull request as ready for review November 13, 2026 05:33
@Peyton-Spencer

Copy link
Copy Markdown
Contributor Author

@ditto review

@ditto-code

ditto-code Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

✅ Ditto reviewed 718b7e8

See this review run → · 7 new comments · approved

Open in Ditto · 5m18s

Comment thread scripts/validate_package.py Resolved
Comment thread plugins/ditto/hooks/ditto-hook.sh Resolved
Comment thread scripts/validate_package.py Resolved
Comment thread scripts/validate_package.py Resolved
Comment thread .github/CODEOWNERS Resolved
Comment thread plugins/ditto/hooks/ditto-hook.sh Resolved
Comment thread plugins/ditto/hooks/ditto-hook.sh Resolved

@ditto-code ditto-code Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto Review

No blocking issues found · risk: medium · 670edd8

File coverage: 3 fully reviewed, 0 reviewed briefly, 0 not reviewed.

Add hash-pinned Claude Code hooks for Ditto memory reminders — Adds Claude Code lifecycle hooks that remind the agent to recall Ditto memory at session start, preserve memory references through compaction, and consider saving outcomes at session end. The reminders stay silent until a Ditto memory tool succeeds; the stop check is gated by attendance, recent activity, and whether memory was already saved. Review the shell hook’s transcript matching and marker-file logic closely, along with the package validator’s hash-pinning and hook restrictions.

Walkthrough (19 files)
File Change
.github/CODEOWNERS Assigns owners to hook code, hook configuration, validation, and GitHub workflow changes.
CONTRIBUTING.md Explains that new plugins start without hooks and that hook files must be hash-pinned.
README.md Documents the Claude Code reminders, opt-out settings, and hook behavior.
RELEASING.md Updates release guidance to cover local reminder hooks and their pinned hashes.
adapters/ditto-memory.md Clarifies that hooks provide reminders but never call Ditto themselves.
docs/hooks.md Describes hook events, safety properties, host support, and verification details.
docs/shared-skills.md Updates the package description to identify the pinned Claude Code hooks as runtime files.
plugins/ditto/.claude-plugin/plugin.json Wires the Ditto Claude Code manifest to its hook configuration.
plugins/ditto/README.md Documents the hooks’ reminders, stop-check conditions, and opt-out controls for plugin users.
plugins/ditto/hooks/claude-hooks.json Configures hooks for successful Ditto memory tools, session start, compaction, and stop events.
plugins/ditto/hooks/ditto-hook.sh Implements local reminders and marker tracking, including a gated end-of-session save check.
plugins/ditto/shared-skills.lock.json Refreshes skill and adapter content hashes to match the updated guidance.
plugins/ditto/skills/ditto-memory/SKILL.md Adds Claude Code hook context while retaining the skill’s memory authorization guidance.
plugins/ditto/skills/tdd/SKILL.md Updates the skill’s description of bundled runtime components.
scripts/hook-allowlist.json Pins the approved hook configuration and script by SHA-256.
scripts/validate_package.py Allows only approved, hash-pinned hooks and validates their configuration, timeouts, and restrictions.
tests/fixtures/claude-code-2.1.308-write.jsonl Adds a Claude Code transcript fixture for testing stop-check tool-call detection.
tests/test_hooks.py Tests hook validation, pinning, opt-outs, reminders, and stop-check behavior.
tests/test_package.py Adapts package tests for the approved hook exception and unhooked new-plugin defaults.
Review details
  • Models: review z-ai/glm-5.3-flash · verify openai/gpt-6-luna
  • Findings: 0 candidates → 0 verified → 0 posted
  • Context index: 72 files · 24 changed symbols · 92 references
  • Tokens: 187771 in / 12140 out · $0.00
  • Incremental since 798b18d
  • Run: 3a14152b-bd46-49aa-ac84-5058cbbaa595 · engine v2.0.0 · details
  • Ditto approves or requests changes in a separate review (blocking: 🟠 High and above). Inference is billed through your Ditto Router endpoint.

Open in Ditto · reply @ditto review to re-run · @ditto fix to push fixes

ditto-code[bot]
ditto-code Bot previously approved these changes Nov 13, 2026

@ditto-code ditto-code Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto: approved — no blocking findings (🟠 High or above) in a complete review of 718b7e8. Findings below that do not block Ditto’s approval.

omarzanji
omarzanji previously approved these changes Nov 13, 2026

@omarzanji omarzanji left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. All three points from my spot-check at a6ce55b are addressed at 718b7e8. What I found beyond them is Low.

Spot-check follow-up

  • Stop hook cost in headless runs: fixed. unattended() skips the check when CLAUDE_CODE_SESSION_ATTENDED=0 or the entrypoint is sdk-*, unless DITTO_HOOKS_STOP=1 is set. Output also moved from decision: block to additionalContext. test_stop_skips_unattended_sessions_unless_asked covers this.
  • Nudging users without Ditto connected: fixed. Every reminder is gated on the connected marker. Only a PostToolUse matched to Ditto memory tools writes that marker, and PostToolUse runs only after a tool succeeds (PostToolUseFailure is a separate event). test_silent_until_a_ditto_memory_tool_has_worked covers this.
  • Transcript key order: fixed. calls() matches either anchor and is pinned to the real 2.1.308 fixture (test_stop_reads_a_real_claude_code_transcript), and test_stop_tolerates_reordered_tool_use_keys covers the reordered case.

I read Ditto Review's seven threads on this head and haven't repeated them. The slash-unescape order in field() and the null/non-string checks in the validator are worth taking.

Low (non-blocking)

  • plugins/ditto/hooks/ditto-hook.sh:89: a user who keeps declining capture is offered it again in every attended session where they edit files, and each offer costs an extra turn over the full context. The skill's "ask before enabling ongoing capture" reads like asking once. Suggestions: cap the offer, for example skip it when any stop-check-* marker is newer than N days, or make the text say "offer only if you have not already been told no in this project".
  • The connected marker never expires. Someone who removes or de-authorizes the Ditto server keeps the SessionStart text and the Stop turn until they set DITTO_HOOKS=0. A 30-day -mtime refresh, like the one the stop markers already get, would let it lapse: PostToolUse touches it again on every Ditto call.
  • Edits made inside subagents land in their own sidechain transcripts, so a session that delegates its file writes only counts the Task calls toward the ten-call threshold. That's fine as a heuristic. One line in docs/hooks.md would explain it.

Rollout (not a finding)

Manifests stay at 0.4.1, so installed users won't get the hooks until a version bump, as the description says. Marketplace-install behaviour is listed as unverified. It's worth one check after the bump, because ${CLAUDE_PLUGIN_DATA} being set is what keeps every hook silent otherwise.

Reviewed by Claude with the teammate-pr-review-loop skill, on behalf of the account posting it.

- validator: reject non-string `hooks` values and hook commands with
  ValueError instead of crashing, and reject allowlist keys that are not
  plugins/<name>/hooks/<file>.
- ditto-hook.sh: unescape JSON paths in one left-to-right pass so a literal
  backslash before a separator survives; skip the connected marker for MCP
  error results; write the Stop marker after the reminder (checking the data
  dir is writable first, so a read-only dir cannot repeat it).
- CODEOWNERS: also cover plugins/*/.claude-plugin/plugin.json, which decides
  which hooks run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ditto-code

ditto-code Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

✅ Ditto reviewed 9bc8e2c

See this review run → · no new comments · approved

Open in Ditto · 1m30s

@Peyton-Spencer

Copy link
Copy Markdown
Contributor Author

@ditto review

omarzanji
omarzanji previously approved these changes Nov 13, 2026

@omarzanji omarzanji left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving again at this head. The delta since my approval of 718b7e8 is the Ditto Review round only, and I checked each part:

  • An MCP error result no longer writes connected. The test covers both isError and is_error, and confirms a non-error result still connects.
  • The Stop marker is now written after the reminder, with the writability check before it. An interrupted run can no longer suppress a reminder that was never shown, and an unwritable data dir still fails quietly instead of repeating the reminder.
  • validate_package.py now rejects non-string hooks and command values, and allowlist entries outside plugins/<name>/hooks/<file> (including .. paths). There are tests for each.
  • CODEOWNERS now also covers plugin.json, which is the file that decides whether hooks are wired at all. That's a good catch.

Low, non-blocking:

  • The isError grep reads the whole hook input, including tool_input. A memory whose text happens to contain "isError": true would not mark Ditto as connected for that call. That fails closed, so it's harmless.
  • The generic \\(.\) unescape turns a JSON \n or \t in a transcript path into n or t. It's fine for real paths, but worth a comment.

My earlier Lows still stand as follow-ups: the capture offer repeats every session, the connected marker never expires, subagent edits don't count toward the threshold, and the version bump.

Reviewed by Claude with the teammate-pr-review-loop skill, on behalf of the account posting it.

ditto-code[bot]
ditto-code Bot previously approved these changes Nov 13, 2026

@ditto-code ditto-code Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto: approved — no blocking findings (🟠 High or above) in a complete review of 9bc8e2c. Findings below that do not block Ditto’s approval.

…se [DITTO-2706]

From Omar's Low notes on #29:

- The Stop check runs at most once a day per install, and the text tells the
  agent not to re-offer capture once the user has declined.
- The `connected` marker lapses after 30 days without a Ditto memory call;
  each call refreshes it, so a de-authorized install goes quiet again.
- docs/hooks.md notes that subagent edits live in sidechain transcripts and
  only their Task calls count toward the ten-call threshold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Peyton-Spencer

Copy link
Copy Markdown
Contributor Author

Thanks @omarzanji. I took all three Lows in 95aaf3f. That push dismisses your approval on 9bc8e2c, sorry for the extra round.

  • Repeated offers. The Stop check now runs at most once a day per install: it is skipped if any stop-check-* marker is under 24 hours old. The text also says to offer capture only "if the user has not already declined it". I chose a daily cap rather than a longer window. A user who has authorized capture still gets the end-of-task nudge on most working days, and the SessionStart text drives saves in between. test_stop_asks_once_per_session_after_real_work now covers the same session, a second session the same day (silent), and the next day.
  • connected expiry. The marker is removed once it is more than 30 days old. PostToolUse touches it on every successful Ditto call, so an install where Ditto is in use never lapses. test_connected_lapses_after_30_days_without_a_ditto_call covers both sides.
  • Subagents. docs/hooks.md now says that only the main transcript is read, and that delegated edits count only through their Task calls.

The 9bc8e2c round also took Ditto's seven threads, including the slash-unescape and null/non-string checks you flagged.

Tests: 42 pass under dash on Boat. Deleting the daily-cap line or the expiry line on a Boat copy fails the matching test.

Rollout note acknowledged. After the version bump, which is Peyton's call, the first check will be a real marketplace install, confirming ${CLAUDE_PLUGIN_DATA} is set.

@Peyton-Spencer

Copy link
Copy Markdown
Contributor Author

@ditto review

@ditto-code

ditto-code Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

✅ Ditto reviewed 95aaf3f

See this review run → · 1 new comment · approved

Open in Ditto · 1m32s

Comment thread plugins/ditto/hooks/ditto-hook.sh Resolved
ditto-code[bot]
ditto-code Bot previously approved these changes Nov 13, 2026

@ditto-code ditto-code Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto: approved — no blocking findings (🟠 High or above) in a complete review of 95aaf3f. Findings below that do not block Ditto’s approval.

omarzanji
omarzanji previously approved these changes Nov 13, 2026

@omarzanji omarzanji left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving again at this head. This commit closes three of my earlier Low notes:

  • Repeated capture offers: capped by the 24-hour stop-check-* mtime check, and the text now tells the agent not to re-offer after a no.
  • connected marker that never expired: each Ditto call now refreshes it, and it is removed after 30 days without one.
  • Subagent edits: now documented as only counting through Task calls toward the ten-call threshold.

Still open as a follow-up: the plugin version bump, so installed users actually receive the hooks.

Reviewed by Claude with the teammate-pr-review-loop skill, on behalf of the account posting it.

find -mtime rounds ages down to whole days, so +30 only matched from day 31.
Use +29 and pin the boundary at 30.01 and 29.9 days in the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Peyton-Spencer

Copy link
Copy Markdown
Contributor Author

@ditto review

@ditto-code

ditto-code Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

✅ Ditto reviewed 798b18d

See this review run → · 1 new comment · approved

Open in Ditto · 1m13s

Comment thread plugins/ditto/hooks/ditto-hook.sh Resolved
ditto-code[bot]
ditto-code Bot previously approved these changes Nov 13, 2026

@ditto-code ditto-code Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto: approved — no blocking findings (🟠 High or above) in a complete review of 798b18d. Findings below that do not block Ditto’s approval.

…ITTO-2706]

find -mtime rounds whole-day ages down on GNU and is documented as rounding up
on BSD, so the daily cap and the 30-day expiry could shift by a day. Use
-mmin -1440 and -mmin +43199, and pin both boundaries in the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
omarzanji
omarzanji previously approved these changes Nov 13, 2026

@omarzanji omarzanji left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving again at this head. The only change since 95aaf3f: -mtime +29 expires the connected marker at 30 days instead of 31, since find rounds ages down to whole days. The tests now bracket the boundary at 29.9 and 30.01 days.

Reviewed by Claude with the teammate-pr-review-loop skill, on behalf of the account posting it.

@Peyton-Spencer

Copy link
Copy Markdown
Contributor Author

@ditto review

@ditto-code

ditto-code Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

✅ Ditto reviewed 670edd8

See this review run → · no new comments · approved

Open in Ditto · 36s

@ditto-code ditto-code Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto: approved — no blocking findings (🟠 High or above) in a complete review of 670edd8. Findings below that do not block Ditto’s approval.

@omarzanji omarzanji left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving again at this head. The marker ages are now counted in minutes (-mmin +43199 and -mmin -1440). GNU and BSD find both support -mmin, and I checked on macOS find that a 43-day-old file matches -mmin +43199 and a fresh one does not. The tests now cover both sides of the daily boundary.

Reviewed by Claude with the teammate-pr-review-loop skill, on behalf of the account posting it.

@Peyton-Spencer
Peyton-Spencer merged commit fd45dc7 into main Nov 13, 2026
2 checks passed
@Peyton-Spencer
Peyton-Spencer deleted the t3/ditto-2706-plugin-hooks branch November 13, 2026 06:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants