Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,14 +695,14 @@ overwrite user-modified agents.
| Agent | Activation | MCP config | Durable context / augmentation |
|-------|------------|------------|--------------------------------|
| Claude Code | Detected | `~/.claude.json` | Skill + three exact-tool graph agents; `SessionStart`, `SubagentStart`, non-blocking `PreToolUse` for `Grep`/`Glob`/`Bash`, and post-`Read` coverage |
| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Managed `AGENTS.md` activation pointer, skill, three read-only agents; `SessionStart` + `SubagentStart` |
| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Managed `AGENTS.md` activation pointer, skill, three read-only agents; `SessionStart` + `SubagentStart`, `PreToolUse` graph lookup for `Bash` searches |
| Gemini CLI | Detected | `.gemini/settings.json` | `GEMINI.md`, three explicit read/graph-tool subagents; `BeforeTool`, `AfterTool` `read_file` coverage, and `SessionStart` |
| Zed | Detected | platform `settings.json` (JSONC) | `AGENTS.md` + shared skill |
| OpenCode | Detected | `$OPENCODE_CONFIG` or resolved global config | `AGENTS.md`, skill, three deny-by-default read-only agents; plugin adds grep/glob graph lookup, post-`read` coverage, first-tool-result session context, and post-compaction reinjection |
| Antigravity | Detected | `.gemini/config/mcp_config.json` | `.gemini/GEMINI.md` |
| Aider | Detected | none | `CONVENTIONS.md` via `.aider.conf.yml` |
| KiloCode | Detected | `.config/kilo/kilo.jsonc` | Rule + three graph-tool subagents with deny-by-default permissions |
| VS Code | Detected | platform `Code/User/mcp.json` | `~/.copilot/skills`, three read-only agents, `sessionStart` + `subagentStart` |
| VS Code | Detected | platform `Code/User/mcp.json` | `~/.copilot/skills`, three read-only agents, `sessionStart` + `subagentStart`, post-search `PostToolUse` graph lookup |
| Cursor | Detected | `.cursor/mcp.json` | Skill + three read-only parent-handoff agents; context hooks withheld because session injection races and `readonly` blocks MCP |
| Windsurf | Detected | `~/.codeium/windsurf/mcp_config.json` | Always-on `global_rules.md` |
| Augment / Auggie | Detected | `~/.augment/settings.json` | Rule, three read-only handoff subagents, `SessionStart` + post-`view` coverage |
Expand All @@ -714,7 +714,7 @@ overwrite user-modified agents.
| Cline | Detected | `~/.cline/mcp.json` + `${CLINE_DATA_DIR:-~/.cline/data}/settings/cline_mcp_settings.json` | Rule + skill; automatic file hooks withheld because they auto-activate and their output is not reliably consumed; child agents cannot use MCP |
| Warp | Detected, skill only | UI, Warp Drive, or per invocation (manual) | Shared `~/.agents/skills/codebase-memory/SKILL.md` |
| Qwen Code | Detected | `.qwen/settings.json` | `QWEN.md`, skill, three explicit read/graph-tool agents; `SessionStart`, `SubagentStart`, and post-`ReadFile` coverage |
| GitHub Copilot CLI | Detected | `$COPILOT_HOME/mcp-config.json` | Instructions, skill, three read-only agents; `sessionStart` + `subagentStart` |
| GitHub Copilot CLI | Detected | `$COPILOT_HOME/mcp-config.json` | Instructions, skill, three read-only agents; `sessionStart` + `subagentStart`, post-search `PostToolUse` graph lookup |
| Factory Droid | Detected | `.factory/mcp.json` | `AGENTS.md`, skill, three droids with exact per-tier graph-tool lists (without additive whole-server exposure); `SessionStart` + post-`Read` coverage on macOS/Linux, withheld on Windows |
| Crush | Detected | `.config/crush/crush.json` | Managed context path with explicit parent-to-child handoff |
| Goose | Detected | `.config/goose/config.yaml` | `.goosehints` |
Expand Down Expand Up @@ -751,7 +751,11 @@ Hooks installed by this project are fail-open and context-only. Claude Code's
`PreToolUse` observes `Grep`/`Glob`/`Bash` and injects matching graph symbols as
`additionalContext`; `PostToolUse` on `Read` adds targeted coverage context when
the graph could not fully parse or index that file. It never denies or replaces
the requested tool call.
the requested tool call. Codex CLI's `PreToolUse` on `Bash` gives its shell
searches the same context. GitHub Copilot CLI and VS Code's Copilot runtime get
it after the search: their `preToolUse` can only decide permissions, so a
`PostToolUse` entry for `Bash`, `Grep` and `Glob` appends the graph symbols to
the tool result.

Claude Code, Codex CLI, Qwen Code, GitHub Copilot CLI, and VS Code's Copilot
runtime receive paired session/subagent context where the vendor exposes a
Expand Down
2 changes: 1 addition & 1 deletion scripts/memory-core-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal/cbm/sqlite_writer.c 74
src/cli/activation_transaction.c 80
src/cli/agent_clients.c 73
src/cli/agent_profiles.c 7
src/cli/cli.c 170
src/cli/cli.c 166
src/cli/client_adapter.c 3
src/cli/config_edit_path.c 2
src/cli/config_json_like.c 142
Expand Down
92 changes: 67 additions & 25 deletions src/cli/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "foundation/compat.h"
#include "foundation/platform.h"
#include "foundation/constants.h"
#include "foundation/json_writer.h"
#include "foundation/log.h"
#include "foundation/mem_core.h"
#include "foundation/sha256.h"
Expand Down Expand Up @@ -4198,7 +4199,8 @@ enum { COPILOT_HOOK_TIMEOUT_SEC = 5 };
static int cbm_build_copilot_hook_command(const char *binary_path, const char *event,
bool powershell, char *out, size_t out_size) {
if (!binary_path || !event || !out ||
(strcmp(event, "SessionStart") != 0 && strcmp(event, "SubagentStart") != 0)) {
(strcmp(event, "SessionStart") != 0 && strcmp(event, "SubagentStart") != 0 &&
strcmp(event, "PostToolUse") != 0)) {
return CLI_ERR;
}
char quoted[CLI_BUF_8K];
Expand All @@ -4215,11 +4217,12 @@ static int cbm_build_copilot_hook_command(const char *binary_path, const char *e
}

static bool cbm_copilot_add_hook_event(yyjson_mut_doc *doc, yyjson_mut_val *hooks,
const char *event_key, const char *bash_command,
const char *powershell_command) {
const char *event_key, const char *matcher,
const char *bash_command, const char *powershell_command) {
yyjson_mut_val *entries = yyjson_mut_arr(doc);
yyjson_mut_val *entry = yyjson_mut_obj(doc);
if (!entries || !entry || !yyjson_mut_obj_add_str(doc, entry, "type", "command") ||
(matcher && !yyjson_mut_obj_add_str(doc, entry, "matcher", matcher)) ||
!yyjson_mut_obj_add_str(doc, entry, "bash", bash_command) ||
!yyjson_mut_obj_add_str(doc, entry, "powershell", powershell_command) ||
!yyjson_mut_obj_add_int(doc, entry, "timeoutSec", COPILOT_HOOK_TIMEOUT_SEC) ||
Expand All @@ -4230,19 +4233,37 @@ static bool cbm_copilot_add_hook_event(yyjson_mut_doc *doc, yyjson_mut_val *hook
return true;
}

static char *cbm_build_copilot_hook_manifest(const char *binary_path) {
/* Copilot CLI's preToolUse cannot add model context (its output is only a
* permission decision) and fails closed on hook errors, so searches are
* augmented after they run: postToolUse additionalContext is appended to the
* tool result the model sees. The PascalCase event name selects the
* Claude-format payload (tool_name Bash/Grep/Glob, tool_input), and the matcher
* names the Claude and the runtime tool names so either matcher reading fires. */
#define COPILOT_SEARCH_MATCHER "Bash|Grep|Glob|bash|powershell|grep|rg|glob"

/* include_posttool renders the current manifest; without it the function
* renders the previous lifecycle-only manifest, which installs up to v0.12 wrote
* and which upgrades and uninstalls must still recognize as owned. The result is
* released with cbm_free(CBM_MEM_CLASS_OTHER, ...). */
static char *cbm_build_copilot_hook_manifest(const char *binary_path, bool include_posttool) {
char session_bash[CLI_BUF_8K];
char session_powershell[CLI_BUF_8K];
char subagent_bash[CLI_BUF_8K];
char subagent_powershell[CLI_BUF_8K];
char post_bash[CLI_BUF_8K];
char post_powershell[CLI_BUF_8K];
if (cbm_build_copilot_hook_command(binary_path, "SessionStart", false, session_bash,
sizeof(session_bash)) != CLI_OK ||
cbm_build_copilot_hook_command(binary_path, "SessionStart", true, session_powershell,
sizeof(session_powershell)) != CLI_OK ||
cbm_build_copilot_hook_command(binary_path, "SubagentStart", false, subagent_bash,
sizeof(subagent_bash)) != CLI_OK ||
cbm_build_copilot_hook_command(binary_path, "SubagentStart", true, subagent_powershell,
sizeof(subagent_powershell)) != CLI_OK) {
sizeof(subagent_powershell)) != CLI_OK ||
cbm_build_copilot_hook_command(binary_path, "PostToolUse", false, post_bash,
sizeof(post_bash)) != CLI_OK ||
cbm_build_copilot_hook_command(binary_path, "PostToolUse", true, post_powershell,
sizeof(post_powershell)) != CLI_OK) {
return NULL;
}

Expand All @@ -4256,39 +4277,59 @@ static char *cbm_build_copilot_hook_manifest(const char *binary_path) {
return NULL;
}
yyjson_mut_doc_set_root(doc, root);
bool ok =
yyjson_mut_obj_add_int(doc, root, "version", 1) &&
cbm_copilot_add_hook_event(doc, hooks, "sessionStart", session_bash, session_powershell) &&
cbm_copilot_add_hook_event(doc, hooks, "subagentStart", subagent_bash,
subagent_powershell) &&
yyjson_mut_obj_add_val(doc, root, "hooks", hooks);
char *manifest = ok ? yyjson_mut_write(doc, YYJSON_WRITE_PRETTY, NULL) : NULL;
bool ok = yyjson_mut_obj_add_int(doc, root, "version", 1) &&
cbm_copilot_add_hook_event(doc, hooks, "sessionStart", NULL, session_bash,
session_powershell) &&
cbm_copilot_add_hook_event(doc, hooks, "subagentStart", NULL, subagent_bash,
subagent_powershell) &&
(!include_posttool ||
cbm_copilot_add_hook_event(doc, hooks, "PostToolUse", COPILOT_SEARCH_MATCHER,
post_bash, post_powershell)) &&
yyjson_mut_obj_add_val(doc, root, "hooks", hooks);
char *manifest = ok ? yyjson_mut_write_opts(doc, YYJSON_WRITE_PRETTY,
cbm_json_writer_allocator(), NULL, NULL)
: NULL;
yyjson_mut_doc_free(doc);
return manifest;
}

#ifdef CBM_CLI_ENABLE_TEST_API
char *cbm_build_copilot_hook_manifest_for_testing(const char *binary_path, bool include_posttool) {
return cbm_build_copilot_hook_manifest(binary_path, include_posttool);
}
#endif

/* Copilot loads every manifest in $COPILOT_HOME/hooks. Treat our dedicated
* filename as an exact-owned document: a foreign collision fails closed, and
* uninstall accepts only the complete generated lifecycle schema. */
* filename as an exact-owned document: a foreign collision fails closed. The
* current manifest or the exact previous one is ours, so install upgrades the
* previous one in place and uninstall removes either. */
static int cbm_upsert_copilot_hooks(const char *binary_path, const char *manifest_path) {
char *manifest = cbm_build_copilot_hook_manifest(binary_path);
if (!manifest || !manifest_path) {
free(manifest);
char *manifest = cbm_build_copilot_hook_manifest(binary_path, true);
char *previous = cbm_build_copilot_hook_manifest(binary_path, false);
if (!manifest || !previous || !manifest_path) {
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
cbm_free(CBM_MEM_CLASS_OTHER, previous);
return CLI_ERR;
}
int rc = cbm_text_ensure_owned_document(manifest_path, manifest);
free(manifest);
const char *const released[] = {previous};
int rc = cbm_text_migrate_owned_document(manifest_path, manifest, released, 1U);
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
cbm_free(CBM_MEM_CLASS_OTHER, previous);
return rc == 0 ? CLI_OK : CLI_ERR;
}

static int cbm_remove_copilot_hooks(const char *manifest_path, const char *binary_path) {
char *manifest = cbm_build_copilot_hook_manifest(binary_path);
if (!manifest || !manifest_path) {
free(manifest);
char *manifest = cbm_build_copilot_hook_manifest(binary_path, true);
char *previous = cbm_build_copilot_hook_manifest(binary_path, false);
if (!manifest || !previous || !manifest_path) {
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
cbm_free(CBM_MEM_CLASS_OTHER, previous);
return CLI_ERR;
}
int rc = cbm_text_remove_owned_document(manifest_path, manifest);
free(manifest);
const char *const released[] = {previous};
int rc = cbm_text_remove_owned_document_any(manifest_path, manifest, released, 1U);
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
cbm_free(CBM_MEM_CLASS_OTHER, previous);
return rc >= 0 ? CLI_OK : CLI_ERR;
}

Expand Down Expand Up @@ -9193,7 +9234,8 @@ static void install_copilot_durable_context(const char *home, const char *binary
record_agent_config_error(false, "Copilot", "lifecycle_hook_install", hook_path);
}
if (hook_ok) {
printf(" hooks: SessionStart + SubagentStart (dynamic graph context)\n");
printf(" hooks: SessionStart + SubagentStart + PostToolUse searches (dynamic graph "
"context)\n");
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/cli/cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ bool cbm_hook_path_contains_for_testing(const char *root, const char *candidate,
bool case_insensitive);
const char *cbm_hook_no_project_index_guidance_for_testing(const char *event);
bool cbm_hook_augment_parse_bash_pattern_for_testing(const char *cmd, char *out, size_t out_sz);
/* include_posttool false renders the previous lifecycle-only manifest. Release
* the result with cbm_free(CBM_MEM_CLASS_OTHER, ...). */
char *cbm_build_copilot_hook_manifest_for_testing(const char *binary_path, bool include_posttool);
bool cbm_mcp_command_path_probe_safe_for_testing(const char *command, bool windows);
void cbm_set_mcp_command_path_probe_counter_for_testing(int *counter);
int cbm_install_editor_mcp_with_previous_for_testing(const char *binary_path,
Expand Down Expand Up @@ -591,8 +594,11 @@ void cbm_hook_augment_arm_deadline(void);
int cbm_hook_augment_deadline_ms_for_testing(void);
#endif
char *cbm_hook_augment_read_stdin(void);
/* Pure no-op gate for the hook-client fast path (see hook_augment.c). */
bool cbm_hook_augment_input_is_noop_bash(const char *input);
/* Pure no-op gate for the hook-client fast path (see hook_augment.c). It also
* covers a forced search event, such as Copilot's PostToolUse; forced_event and
* dialect_name are borrowed and may be NULL. */
bool cbm_hook_augment_input_is_noop_for(const char *input, const char *forced_event,
const char *dialect_name);
/* Hand back stdin bytes main() consumed early; the next read returns them. */
void cbm_hook_augment_prefetch_stdin(char *owned);

Expand Down
Loading
Loading