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
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning: [S

## [Unreleased]

## [0.24.2] - 2026-07-12

### Fixed
- Codex plugin 0.1.2 encodes SessionStart/SessionEnd hooks as the documented
single command string. Codex previously ignored the unsupported `args` array,
ran bare `sh`, and fed hook JSON to it as shell input, causing exit code 127.

## [0.24.1] - 2026-07-12

### Fixed
Expand Down Expand Up @@ -358,7 +365,8 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning: [S
- Out-of-band capture from coding-agent transcripts (redacted, non-lossy `remember`).
- Published to PyPI via GitHub Trusted Publishing (OIDC, no stored secrets).

[Unreleased]: https://github.com/ccf/agentcairn/compare/v0.24.1...HEAD
[Unreleased]: https://github.com/ccf/agentcairn/compare/v0.24.2...HEAD
[0.24.2]: https://github.com/ccf/agentcairn/compare/v0.24.1...v0.24.2
[0.24.1]: https://github.com/ccf/agentcairn/compare/v0.24.0...v0.24.1
[0.24.0]: https://github.com/ccf/agentcairn/compare/v0.23.0...v0.24.0
[0.23.0]: https://github.com/ccf/agentcairn/compare/v0.22.1...v0.23.0
Expand Down
2 changes: 1 addition & 1 deletion plugin/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentcairn",
"version": "0.1.1",
"version": "0.1.2",
"description": "Local-first agent memory for Codex — recall, remember, and ambient capture into a Markdown vault you own.",
"author": { "name": "Charles C. Figueiredo", "email": "ccf@ccf.io" },
"homepage": "https://agentcairn.dev",
Expand Down
8 changes: 4 additions & 4 deletions plugin/hooks/hooks.codex.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command", "command": "sh",
"args": ["${PLUGIN_ROOT}/scripts/session-start.sh"], "timeout": 20 } ] }
{ "hooks": [ { "type": "command",
"command": "/bin/sh \"${PLUGIN_ROOT}/scripts/session-start.sh\"", "timeout": 20 } ] }
],
"SessionEnd": [
{ "hooks": [ { "type": "command", "command": "sh",
"args": ["${PLUGIN_ROOT}/scripts/session-end.sh"], "timeout": 120 } ] }
{ "hooks": [ { "type": "command",
"command": "/bin/sh \"${PLUGIN_ROOT}/scripts/session-end.sh\"", "timeout": 120 } ] }
]
}
}
48 changes: 48 additions & 0 deletions plugin/tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,54 @@ def test_mcp_config_wires_uvx_agentcairn():
assert srv["env"]["CAIRN_VAULT"] == "${user_config.vault_path}"


def test_codex_hooks_use_documented_single_command_strings():
hooks = _json(PLUGIN / "hooks" / "hooks.codex.json")["hooks"]
expected = {
"SessionStart": '/bin/sh "${PLUGIN_ROOT}/scripts/session-start.sh"',
"SessionEnd": '/bin/sh "${PLUGIN_ROOT}/scripts/session-end.sh"',
}
for event, command in expected.items():
handler = hooks[event][0]["hooks"][0]
assert handler["command"] == command
assert "args" not in handler


def test_codex_session_start_handler_executes_script_instead_of_stdin(tmp_path):
hooks = _json(PLUGIN / "hooks" / "hooks.codex.json")["hooks"]
handler = hooks["SessionStart"][0]["hooks"][0]
scripts = tmp_path / "scripts"
scripts.mkdir()
stub = scripts / "session-start.sh"
stub.write_text(
"#!/bin/sh\n"
'grep -q \'"hook_event_name": "SessionStart"\' || exit 2\n'
"printf '%s\\n' "
'\'{"hookSpecificOutput":{"hookEventName":"SessionStart",'
'"additionalContext":"Codex hook works"}}\'\n'
)
stub.chmod(0o755)
env = {
**os.environ,
"PLUGIN_ROOT": str(tmp_path),
}

result = subprocess.run(
handler["command"],
shell=True,
executable="/bin/sh",
input=json.dumps({"hook_event_name": "SessionStart", "cwd": str(ROOT)}),
capture_output=True,
text=True,
env=env,
timeout=10,
)

assert result.returncode == 0, result.stderr
output = json.loads(result.stdout)
assert output["hookSpecificOutput"]["hookEventName"] == "SessionStart"
assert "Codex hook works" in output["hookSpecificOutput"]["additionalContext"]


def _run_hook(script, stdin_obj, env_extra, cwd=None, first_run=False):
env = {**os.environ, **env_extra}
# The index is now vault-derived, so session-start.sh detects a first run via the
Expand Down
2 changes: 1 addition & 1 deletion src/cairn/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: Apache-2.0
"""agentcairn — local-first agent memory (import package `cairn`)."""

__version__ = "0.24.1"
__version__ = "0.24.2"
8 changes: 4 additions & 4 deletions tests/test_plugin_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ def test_codex_mcp_is_bare_map_and_defers_to_shared_config():

def test_codex_hooks_reference_existing_scripts():
h = _load(PLUGIN / "hooks" / "hooks.codex.json")
starts = h["hooks"]["SessionStart"][0]["hooks"][0]["args"]
ends = h["hooks"]["SessionEnd"][0]["hooks"][0]["args"]
assert starts == ["${PLUGIN_ROOT}/scripts/session-start.sh"]
assert ends == ["${PLUGIN_ROOT}/scripts/session-end.sh"]
starts = h["hooks"]["SessionStart"][0]["hooks"][0]["command"]
ends = h["hooks"]["SessionEnd"][0]["hooks"][0]["command"]
assert starts == '/bin/sh "${PLUGIN_ROOT}/scripts/session-start.sh"'
assert ends == '/bin/sh "${PLUGIN_ROOT}/scripts/session-end.sh"'
assert (PLUGIN / "scripts" / "session-start.sh").is_file()
assert (PLUGIN / "scripts" / "session-end.sh").is_file()

Expand Down