Skip to content

Repo-local .honcho configuration and per-repo sessions - #12

Open
outpoints wants to merge 2 commits into
plastic-labs:mainfrom
outpoints:repo-local-config
Open

Repo-local .honcho configuration and per-repo sessions#12
outpoints wants to merge 2 commits into
plastic-labs:mainfrom
outpoints:repo-local-config

Conversation

@outpoints

@outpoints outpoints commented Jul 14, 2026

Copy link
Copy Markdown

Summary

Adds an optional repository-local .honcho/config.json overlay so each project can route Codex memory to its own Honcho workspace while inheriting credentials and defaults from the global ~/.honcho/config.json.

This ports the approach from plastic-labs/claude-honcho#44 and adds the Hermes-compatible per-repo session strategy.

Why

The Codex integration currently resolves Honcho configuration globally. When working across multiple repositories, messages, tool activity, and derived conclusions all land in the same workspace, allowing unrelated project context to bleed together.

The existing hosted MCP registration also cannot follow the active working directory: its credentials and routing are fixed in Codex configuration at install time. A local overlay would therefore affect hooks but not MCP recall tools unless MCP configuration resolution moved into a cwd-aware local process.

How it works

  • Overlay, not replacement. The nearest .honcho/config.json at or above the working directory is layered over the global config. Omitted values continue to inherit globally, so a repository file can usually contain only { "workspace": "my-project" }.
  • Host-aware resolution. Both flat fields and hosts.codex are supported, with environment-provided API keys retaining highest precedence.
  • Read-only repository configuration. codex-honcho never writes the local file or copies its values into global configuration.
  • Project-root sessions. With a local config active, session identity anchors to the directory containing .honcho/, preventing subdirectories from unintentionally creating separate sessions.
  • Hermes-compatible per-repo strategy. All directories in the same Git repository share the Git root’s session name. Outside Git it falls back to per-directory; worktrees and submodules with .git files are supported.
  • Optional session controls. sessionName pins an exact session, while splitSubmodules lets other strategies give nested repositories independent anchors.
  • Cwd-aware local MCP server. Codex now launches a credential-free local stdio server that resolves the active repository configuration before calling Honcho. Hooks and MCP tools therefore use the same endpoint, workspace, and identity.
  • Isolated local state. Queue, cursor, and context keys are namespaced by the resolved local workspace so pending global messages cannot cross into a project workspace.

Compatibility and security

The feature is additive and opt-in. Without a repository-local .honcho/config.json, existing global configuration and session behavior remain unchanged.

API keys no longer need to appear in Codex’s MCP configuration. Repository files should contain only non-secret routing settings; .env files are explicitly ignored.

Verification

  • bun test — 92 tests, 207 expectations
  • bun run typecheck
  • bun run build
  • npm pack --dry-run
  • Local MCP handshake verified with all eight Honcho tools
  • Live read-only SDK request verified against a configured self-hosted endpoint
  • Staged diff and secret scans clean

Summary by CodeRabbit

  • New Features

    • Added repository-local configuration overlays with per-repository session strategies, submodule support, and isolated memory.
    • Added a credential-free local MCP server with tools for search, chat, context, representations, and conclusions.
    • MCP setup now launches the local server automatically when local configuration is available.
  • Documentation

    • Updated setup, configuration, troubleshooting, and development guidance for local workflows.
  • Chores

    • Added safeguards to ignore environment files while retaining the example configuration.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds repository-local Honcho configuration overlays, per-repository session and memory-key handling, and a local stdio MCP server. The CLI, hooks, connector, tests, and documentation now use directory-aware configuration.

Changes

Repository-local configuration

Layer / File(s) Summary
Configuration resolution and session scoping
src/config.ts, test/config.test.ts, .gitignore, CHANGELOG.md, README.md
loadConfig(cwd) resolves local overlays, Git roots, session strategies, submodule settings, and local memory namespaces. Tests cover precedence, malformed files, nested repositories, and persistence.
Local MCP server and Codex registration
src/mcp.ts, src/connectors/mcp.ts, test/mcp.test.ts, test/connectors/mcp.test.ts, build.mjs, README.md
The MCP server exposes Honcho tools over stdio. Codex stores executable command descriptors instead of hosted URLs and credentials.
CLI and hook integration
bin/codex-honcho.ts, src/hooks/*, README.md
The CLI and hooks pass the working directory to configuration loading. The mcp command keeps the server running, and installation registers the local command.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Codex
  participant CLI
  participant LocalMCP
  participant Config
  participant Honcho
  Codex->>CLI: run install or mcp
  CLI->>LocalMCP: launch local stdio server
  LocalMCP->>Config: loadConfig(cwd)
  LocalMCP->>Honcho: execute selected tool
  Honcho-->>LocalMCP: return result
  LocalMCP-->>Codex: return MCP response
Loading

Possibly related PRs

Suggested reviewers: erosika

Poem

A rabbit found a local root,
And stored its keys beneath the route.
It launched a server, quiet and bright,
With Honcho tools on stdio light.
“No stray secrets!” the rabbit sings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: repository-local .honcho configuration and per-repository session strategies.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@outpoints
outpoints marked this pull request as ready for review August 6, 2026 20:30

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bin/codex-honcho.ts`:
- Around line 115-117: Update the configuration flow around loadConfig and
installMcpServer: store the loaded config, then install the MCP server only when
config?.enabled is true. Preserve the existing mcpInvoke(entry) registration and
success message for enabled configurations, while skipping installation when
enabled is false or no config is loaded.

In `@src/config.ts`:
- Around line 319-325: Update memoryKey to include localConfig.root in the local
key alongside the workspace and key, ensuring repositories sharing a workspace
remain isolated when sessionId is absent. Preserve the existing non-local key
behavior and the current sessionName fallback.
- Around line 181-184: Update applyLocalConfig’s LOCAL_FIELDS overlay to
normalize and validate each local value before assigning it: coerce
enabled-style fields using the existing boolean semantics, accept
sessionStrategy only when it matches an allowed value, and pass local peerName
through resolvePeerName just as the global configuration path does. Do not write
raw localField results through the unknown cast.
- Around line 165-191: Update applyLocalConfig so a repository-local endpoint
override is applied only when the local configuration also provides its own
apiKey; otherwise preserve the inherited endpoint while allowing other local
fields to merge. Ensure the existing environment-key override and
effective.apiKey validation remain intact, and prevent localField(raw,
"endpoint") from replacing the trusted endpoint when the key would be inherited
from base.

In `@src/mcp.ts`:
- Around line 150-153: The Honcho construction in src/mcp.ts lines 150-153 must
prevent repository-local endpoint configuration from inheriting global or
environment credentials: restrict local endpoint overrides or require an
explicit trusted opt-in before passing credentials to Honcho, while preserving
normal trusted configurations. Update README.md lines 138-151 to remove the
unsafe inheritance claim and document the enforced endpoint and credential trust
rule.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a4678da9-3397-4ecb-8294-d1b14d86451d

📥 Commits

Reviewing files that changed from the base of the PR and between c8d35e6 and 2f909f5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • .gitignore
  • CHANGELOG.md
  • README.md
  • bin/codex-honcho.ts
  • build.mjs
  • src/config.ts
  • src/connectors/mcp.ts
  • src/hooks/flush.ts
  • src/hooks/observe.ts
  • src/hooks/prompt.ts
  • src/hooks/recall.ts
  • src/hooks/writeback.ts
  • src/mcp.ts
  • test/config.test.ts
  • test/connectors/mcp.test.ts
  • test/mcp.test.ts

Comment thread bin/codex-honcho.ts
Comment on lines +115 to +117
if (loadConfig(process.cwd())) {
installMcpServer(mcpInvoke(entry));
console.log(`Registered Honcho MCP server → ${DEFAULT_CONFIG_PATH}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/config.ts --items all
rg -n -C 5 'loadConfig|enabled|apiKey|resolveFileConfig' src/config.ts test

Repository: plastic-labs/codex-honcho

Length of output: 30837


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== bin/codex-honcho outline =="
ast-grep outline bin/codex-honcho.ts --items all || true

echo "== relevant bin/codex-honcho.ts lines =="
sed -n '1,180p' bin/codex-honcho.ts | cat -n

echo "== mcp connectors outline/search =="
fd -i '.*mcp.*' .
rg -n -C 4 'runMcpServer|installMcpServer|loadConfig\(|config\.enabled|enabled:' .

Repository: plastic-labs/codex-honcho

Length of output: 31773


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== config resolver behavior probe =="
python3 - <<'PY'
from pathlib import Path
import re

text = Path('src/config.ts').read_text()

checks = {
    "default enabled from file": ",enabled: ",
    "local enabled is a local field": '"enabled"',
    "default truthiness": "('enabled' in raw) === false)",
}
for name, needle in checks.items():
    print(f"{name}: {needle in text} ({text.find(needle)})")

apply = re.search(r"function applyLocalConfig\([\s\S]*?\n\}", text).group(0)
enabled_assign_in_apply = bool(re.search(r"\(effective as unknown[\s\S]*?\[\"enabled\"\][\s\S]*?local\.", apply))
print("local assigned into effective.enabled:", enabled_assign_in_apply)
PY

echo "== deterministic semantic extraction =="
python3 - <<'PY'
exec("function resolveFileConfig(raw) {\n  const HOST = 'codex';\n  const apiKey = process.env.HONCHO_API_KEY || raw.hosts?.[HOST]?.apiKey || raw.apiKey;\n  if (!apiKey) return null;\n  return { apiKey, enabled: (raw.hosts?.codex?.enabled ?? raw.enabled) !== false };\n}")
cases = [
    {},
    {"apiKey": "k", "peerName": "u"},
    {"apiKey": "k", "peerName": "u", "enabled": false},
    {"hosts": {"codex": {"apiKey": "k", "enabled": false}}},
    {"hosts": {"codex": {"apiKey": "k"}}},
]
for raw in cases:
    try:
        process={"HONCHO_API_KEY":""}
        out=resolveFileConfig(raw)
        print(raw, "=>", out)
    except Exception as e:
        print(raw, "=> ERROR", e)
PY

Repository: plastic-labs/codex-honcho

Length of output: 578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '92,195p' src/config.ts | cat -n

node - <<'JS'
function resolveFileConfig(raw) {
  const HOST = "codex";
  const apiKey = process.env.HONCHO_API_KEY || raw.hosts?.[HOST]?.apiKey || raw.apiKey;
  if (!apiKey) return null;
  return { apiKey, enabled: (raw.hosts?.codex?.enabled ?? raw.enabled) !== false };
}

function applyLocalConfig(base, localEnabled) {
  if (!base) return null;
  const effective = { ...base };
  if (localEnabled !== undefined) effective.enabled = localEnabled;
  if (!effective.apiKey) return base;
  return effective;
}

for (const [description, [base, localEnabled]] of Object.entries({
  "global empty default": [{apiKey:"k",peerName:"u"}, undefined],
  "global explicit enabled=false": [{apiKey:"k",peerName:"u", enabled:false}, undefined],
  "global hosts.codex enabled=false": [{apiKey:"k",peerName:"u", hosts: {codex: {enabled:false}}}, undefined],
  "local overlay enabled=false": [{apiKey:"k",peerName:"u"}, false],
})) {
  const cfg = applyLocalConfig(resolveFileConfig(base), localEnabled);
  console.log(JSON.stringify({description, config: cfg}));
}
JS

Repository: plastic-labs/codex-honcho

Length of output: 4762


Gate install on Config.enabled.

enabled can be explicitly false at the global root, hosts.codex, or a repo-local overlay; loadConfig() can still return that config if an API key is present. runMcpServer uses config.enabled before starting, so install can register a server that cannot start. Store the loaded config and install only when config?.enabled is true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/codex-honcho.ts` around lines 115 - 117, Update the configuration flow
around loadConfig and installMcpServer: store the loaded config, then install
the MCP server only when config?.enabled is true. Preserve the existing
mcpInvoke(entry) registration and success message for enabled configurations,
while skipping installation when enabled is false or no config is loaded.

Comment thread src/config.ts
Comment on lines +165 to +191
function applyLocalConfig(base: Config | null, local: LocalConfigInfo | null): Config | null {
if (!local) return base;

let raw: FileConfig;
try {
raw = JSON.parse(readFileSync(local.path, "utf-8")) as FileConfig;
} catch {
return base;
}

// A local file normally inherits credentials and identity from the shared
// config, but it can also be fully self-contained when no global config is
// available.
const effective = base ? { ...base } : resolveFileConfig(raw);
if (!effective) return base;

for (const key of LOCAL_FIELDS) {
const value = localField(raw, key);
if (value !== undefined) (effective as unknown as Record<string, unknown>)[key] = value;
}
// Preserve the existing explicit environment override above both config
// files; a repo-local apiKey still works when HONCHO_API_KEY is absent.
if (process.env.HONCHO_API_KEY) effective.apiKey = process.env.HONCHO_API_KEY;
if (!effective.apiKey) return base;
effective.localConfig = local;
return effective;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Restrict inherited credentials when a repo-local file overrides endpoint.

applyLocalConfig copies endpoint from a file that lives inside the repository. The apiKey is inherited from the global config in that same object. A cloned repository can therefore ship .honcho/config.json with hosts.codex.endpoint.baseUrl set to an attacker-controlled host. The next hook run in that directory sends the user's global Honcho API key and the captured transcript data to that host through honchoClientOptions. Discovery is automatic, so no user action confirms the overlay.

Suggested mitigations, in order of preference:

  1. Ignore a local endpoint unless the local file also supplies its own apiKey.
  2. Restrict a local endpoint.baseUrl to loopback hosts, and require the global file to opt in for anything else.
  3. Require an explicit trust record (for example a list of approved local config paths in the global file) before any local overlay applies.
🔒 Minimal guard: do not pair an inherited key with a local endpoint
   for (const key of LOCAL_FIELDS) {
     const value = localField(raw, key);
     if (value !== undefined) (effective as unknown as Record<string, unknown>)[key] = value;
   }
+  // A local endpoint must not receive credentials inherited from ~/.honcho/config.json.
+  const localApiKey = localField(raw, "apiKey");
+  if (localField(raw, "endpoint") !== undefined && !localApiKey) {
+    effective.endpoint = base?.endpoint;
+  }
   // Preserve the existing explicit environment override above both config
   // files; a repo-local apiKey still works when HONCHO_API_KEY is absent.
   if (process.env.HONCHO_API_KEY) effective.apiKey = process.env.HONCHO_API_KEY;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function applyLocalConfig(base: Config | null, local: LocalConfigInfo | null): Config | null {
if (!local) return base;
let raw: FileConfig;
try {
raw = JSON.parse(readFileSync(local.path, "utf-8")) as FileConfig;
} catch {
return base;
}
// A local file normally inherits credentials and identity from the shared
// config, but it can also be fully self-contained when no global config is
// available.
const effective = base ? { ...base } : resolveFileConfig(raw);
if (!effective) return base;
for (const key of LOCAL_FIELDS) {
const value = localField(raw, key);
if (value !== undefined) (effective as unknown as Record<string, unknown>)[key] = value;
}
// Preserve the existing explicit environment override above both config
// files; a repo-local apiKey still works when HONCHO_API_KEY is absent.
if (process.env.HONCHO_API_KEY) effective.apiKey = process.env.HONCHO_API_KEY;
if (!effective.apiKey) return base;
effective.localConfig = local;
return effective;
}
function applyLocalConfig(base: Config | null, local: LocalConfigInfo | null): Config | null {
if (!local) return base;
let raw: FileConfig;
try {
raw = JSON.parse(readFileSync(local.path, "utf-8")) as FileConfig;
} catch {
return base;
}
// A local file normally inherits credentials and identity from the shared
// config, but it can also be fully self-contained when no global config is
// available.
const effective = base ? { ...base } : resolveFileConfig(raw);
if (!effective) return base;
for (const key of LOCAL_FIELDS) {
const value = localField(raw, key);
if (value !== undefined) (effective as unknown as Record<string, unknown>)[key] = value;
}
// A local endpoint must not receive credentials inherited from ~/.honcho/config.json.
const localApiKey = localField(raw, "apiKey");
if (localField(raw, "endpoint") !== undefined && !localApiKey) {
effective.endpoint = base?.endpoint;
}
// Preserve the existing explicit environment override above both config
// files; a repo-local apiKey still works when HONCHO_API_KEY is absent.
if (process.env.HONCHO_API_KEY) effective.apiKey = process.env.HONCHO_API_KEY;
if (!effective.apiKey) return base;
effective.localConfig = local;
return effective;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config.ts` around lines 165 - 191, Update applyLocalConfig so a
repository-local endpoint override is applied only when the local configuration
also provides its own apiKey; otherwise preserve the inherited endpoint while
allowing other local fields to merge. Ensure the existing environment-key
override and effective.apiKey validation remain intact, and prevent
localField(raw, "endpoint") from replacing the trusted endpoint when the key
would be inherited from base.

Comment thread src/config.ts
Comment on lines +181 to +184
for (const key of LOCAL_FIELDS) {
const value = localField(raw, key);
if (value !== undefined) (effective as unknown as Record<string, unknown>)[key] = value;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep run --pattern 'function resolvePeerName($$$) { $$$ }' --lang typescript src/config.ts
rg -n 'resolvePeerName' --type=ts

Repository: plastic-labs/codex-honcho

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- files ---\n'
git ls-files | rg '(^|/)src/config\.ts$|package\.json|tsconfig\.json' || true

printf '\n--- outline ---\n'
ast-grep outline src/config.ts --view compact || true

printf '\n--- relevant symbols ---\n'
rg -n 'resolvePeerName|sessionName|LOCAL_FIELDS|localField|peerName|sessionStrategy|enabled' src/config.ts || true

printf '\n--- src/config.ts relevant sections ---\n'
sed -n '1,140p' src/config.ts
printf '\n--- 150-230 ---\n'
sed -n '150,230p' src/config.ts
printf '\n--- 230-340 ---\n'
sed -n '230,340p' src/config.ts

Repository: plastic-labs/codex-honcho

Length of output: 254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- files ---'
git ls-files | rg '(^|/)src/config\.ts$|package\.json|tsconfig\.json' || true

printf '%s\n' ''
printf '%s\n' '--- outline ---'
ast-grep outline src/config.ts --view compact || true

printf '%s\n' ''
printf '%s\n' '--- relevant symbols ---'
rg -n 'resolvePeerName|sessionName|LOCAL_FIELDS|localField|peerName|sessionStrategy|enabled' src/config.ts || true

printf '%s\n' ''
printf '%s\n' '--- src/config.ts relevant sections ---'
sed -n '1,140p' src/config.ts
printf '%s\n' ''
printf '%s\n' '--- 150-230 ---'
sed -n '150,230p' src/config.ts
printf '%s\n' ''
printf '%s\n' '--- 230-340 ---'
sed -n '230,340p' src/config.ts

Repository: plastic-labs/codex-honcho

Length of output: 13811


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const enabledTruthinessChecks = [undefined, false, "false", 0, null, "", true, "true", 1, {}];
for (const value of enabledTruthinessChecks) {
  const result = value !== false;
  console.log(JSON.stringify(value) + " -> enabled: " + Boolean(result) + " (value !== false is true)");
}

const strategies = ["per-directory", "per-repo", "git-branch", "chat-instance", "bad-strategy", null, true, 123, {}];
for (const strategy of strategies) {
  let name;
  const directoryAnchor = "/tmp/repo";
  const anchor = strategy === "per-repo" ? directoryAnchor : directoryAnchor;
  const repo = strategy.toString().toLowerCase().replace(/[^a-z0-9-_]/g, "-");
  try {
    switch (strategy) {
      case "git-branch":
        name = repo;
        break;
      case "chat-instance":
        name = repo;
        break;        
      case "per-repo":
      case "per-directory":
      default:
        name = repo;
    }
  } catch (e) {
    name = `ERROR: ${e.message}`;
  }
  console.log(JSON.stringify(strategy) + " -> sessionName uses strategy branch `" + (strategy === "git-branch" ? "git-branch" : strategy === "chat-instance" ? "chat-instance" : "default") + "` -> `" + name + "`");
}
JS

Repository: plastic-labs/codex-honcho

Length of output: 1728


Normalize and validate local config fields before applying the overlay.

applyLocalConfig writes raw JSON values through unknown, so invalid types can reach global fallback code. "enabled": "false" becomes enabled, "enabled": 0 becomes enabled, and an invalid sessionStrategy is accepted by the default branch. Local peerName also bypasses resolvePeerName. Coerce booleans, validate sessionStrategy against the allowed values before use, and normalize local peerName like the global file path does.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config.ts` around lines 181 - 184, Update applyLocalConfig’s LOCAL_FIELDS
overlay to normalize and validate each local value before assigning it: coerce
enabled-style fields using the existing boolean semantics, accept
sessionStrategy only when it matches an allowed value, and pass local peerName
through resolvePeerName just as the global configuration path does. Do not write
raw localField results through the unknown cast.

Comment thread src/config.ts
Comment on lines 319 to 325
export function memoryKey(config: Config, cwd: string, sessionId?: string): string {
return sessionId || sessionName(config, cwd, sessionId);
const key = sessionId || sessionName(config, cwd, sessionId);
// Keep queues/cursors/context from an existing global session out of a newly
// activated repo-local workspace. The default path remains byte-for-byte
// compatible with prior releases.
return config.localConfig ? `local-${config.workspace}-${key}` : key;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'memoryKey\(' --type=ts

Repository: plastic-labs/codex-honcho

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -e

echo "git status/files relevant:"
git ls-files | rg '(^src/config\.ts$|^bin/|config|package|README)' | sed -n '1,200p'

echo
echo "find config.ts and honcho:"
fd -a 'config\.ts$|codex-honcho\.ts$' . 2>/dev/null || true

echo
echo "search memoryKey text:"
rg -n -C3 'memoryKey|function memoryKey|const.*memoryKey' . 2>/dev/null || true

echo
echo "config.ts outline snippets around lines:"
if [ -f src/config.ts ]; then
  wc -l src/config.ts
  sed -n '1,380p' src/config.ts
fi

Repository: plastic-labs/codex-honcho

Length of output: 19668


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

config = Path("src/config.ts").read_text()
honcho = Path("bin/codex-honcho.ts").read_text()

fn = re.search(r'export function memoryKey\([\s\S]*?\n\}', config)
calls = []
for m in re.finditer(r'memoryKey\s*\((.+?)\)', honcho + "\n" + config):
    calls.append(m.group(0))

fn_text = fn.group(0) if fn else "<missing>"

print("memoryKey implementation:")
print(fn_text)

print("\nmemoryKey call sites:")
for c in calls:
    print(c)

def memory_key(workspace, session_name, local_root, session_id):
    key = session_id or session_name
    return f"local-{workspace}-{key}" if local_root else key

cases = [
    {"name": "same local workspace/session_name, missing session_id, different roots",
     "w1": "my-ws", "sn1": "my-project", "root1": "/repo-a",
     "w2": "my-ws", "sn2": "my-project", "root2": "/repo-b", "session_id": None},
    {"name": "same local workspace/session_name, same session_id, different roots",
     "w1": "my-ws", "sn1": "my-project", "root1": "/repo-a",
     "w2": "my-ws", "sn2": "my-project", "root2": "/repo-b", "session_id": "abc"},
    {"name": "local workspace plus different session_name/session_id",
     "w1": "my-ws", "sn1": "my-project", "root1": "/repo-a",
     "w2": "my-ws", "sn2": "other-project", "root2": "/repo-b", "session_id": "xyz"},
]
for case in cases:
    k1 = memory_key(case["w1"], case["sn1"], case["root1"], case["session_id"])
    k2 = memory_key(case["w2"], case["sn2"], case["root2"], case["session_id"])
    print(f'\n{case["name"]}')
    print(f'key1={k1}')
    print(f'key2={k2}')
    print(f'collision={k1==k2}')
PY

Repository: plastic-labs/codex-honcho

Length of output: 1098


Use another stable discriminator when the same local workspace is reused.

memoryKey uses only local-<workspace>-<key>, so distinct repositories with the same local workspace and missing session_id share queue, cursor, and context files. Include localConfig.root in the local key if repo-local state should remain isolated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config.ts` around lines 319 - 325, Update memoryKey to include
localConfig.root in the local key alongside the workspace and key, ensuring
repositories sharing a workspace remain isolated when sessionId is absent.
Preserve the existing non-local key behavior and the current sessionName
fallback.

Comment thread src/mcp.ts
Comment on lines +150 to +153
const config = loadConfig(cwd);
if (!config || !config.enabled) throw new Error("Honcho is not configured for this workspace");

const honcho = new Honcho(honchoClientOptions(config));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Prevent repository-local endpoints from receiving inherited credentials.

A cloned repository can supply .honcho/config.json with an attacker-controlled endpoint. The resolver can then combine that endpoint with HONCHO_API_KEY or the global API key. The SDK client sends the inherited credential to that endpoint when a hook or MCP tool makes a request.

  • src/mcp.ts#L150-L153: enforce credential provenance before constructing Honcho. Do not send a global or environment API key to an endpoint selected by repository-local configuration. Restrict local endpoint overrides or require an explicit trusted opt-in.
  • README.md#L138-L151: remove the claim that local endpoint overrides safely inherit omitted credentials until the resolver enforces this boundary. Document the resulting trust rule.
📍 Affects 2 files
  • src/mcp.ts#L150-L153 (this comment)
  • README.md#L138-L151
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp.ts` around lines 150 - 153, The Honcho construction in src/mcp.ts
lines 150-153 must prevent repository-local endpoint configuration from
inheriting global or environment credentials: restrict local endpoint overrides
or require an explicit trusted opt-in before passing credentials to Honcho,
while preserving normal trusted configurations. Update README.md lines 138-151
to remove the unsafe inheritance claim and document the enforced endpoint and
credential trust rule.

@oliver-mee

Copy link
Copy Markdown

This PR directly addresses an important self-hosted Honcho compatibility gap. The local stdio MCP approach is the right direction, and I support merging it.

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