Skip to content
Open
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
17 changes: 14 additions & 3 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Codex has two supported modes. This choice applies only to codex; Claude Code re

## 1. Kernel server

Outcome: the clikernel MCP server is registered. Claude Code: a user-scope server named `clikernel` running `<venv>/bin/clikernel-mcp`. Kernel-centric codex: a `[mcp_servers.clikernel]` block in `~/.codex/config.toml` with `command` set to that binary, `startup_timeout_sec = 30`, `tool_timeout_sec = 3600`, and `approval_mode = "approve"` for its `execute`, `connect`, `restart`, and `interrupt` tools.
Outcome: the clikernel MCP server is registered. Claude Code: a user-scope server named `clikernel` running `<this repo>/scripts/clikernel-mcp-shim`, which execs `<venv>/bin/clikernel-mcp`, adding `--quiet` when `CLAUDE_CODE_ENTRYPOINT` is `claude-desktop`: desktop kernels skip the startup notice, since desktop sessions take their instructions from the hooks (step 3). Kernel-centric codex: a `[mcp_servers.clikernel]` block in `~/.codex/config.toml` with `command` set to that binary, `startup_timeout_sec = 30`, `tool_timeout_sec = 3600`, and `approval_mode = "approve"` for its `execute`, `connect`, `restart`, and `interrupt` tools.

Hybrid codex: use the following exact working configuration, changing the `command` path if the workspace is elsewhere:

Expand Down Expand Up @@ -59,6 +59,8 @@ Settle first: existing non-symlink files at those paths.

Outcome, Claude Code, in `~/.claude/settings.json` under `hooks`: PreToolUse matcher `Write|Edit|NotebookEdit` runs `aai-hook claude-block-native-edit`; PreToolUse matcher `Bash` runs `aai-hook claude-bash-guard`; UserPromptSubmit runs `aai-hook claude-prompt-submit`; SessionStart runs `aai-hook claude-session-start`; UserPromptSubmit, MessageDisplay, and PostToolBatch each also run `aai-hook claude-air` (the come-up-for-air nudge: after 8 tool-call rounds with no text response of 100+ chars, it injects a reminder to surface and reassess, repeating every 5 further rounds). The air nudge is Claude-only: codex has no message-level hook event, so it cannot observe the "text happened" reset condition - the codex-shaped substitute is a sentence in AGENTS.md; revisit if codex grows one. PostToolBatch and Stop also each run `aai-hook claude-drop-sentinel`, a Python port of podlayer/message-drop-sentinel (MIT): it detects the thinking-sandwich message-drop platform bug from the transcript scar (two adjacent thinking blocks) and tells the agent its text was probably eaten: restate it in the turn-final message, or say it now and end the turn if the user needs it immediately. Retire the sentinel entries when the upstream bug is fixed (re-test recipe and issue links in that repo's README). UserPromptSubmit and MessageDisplay also each run `aai-hook claude-slop`: MessageDisplay buffers each displayed assistant message, and at the next prompt the hook scores the previous turn's final message with the `slopometer` CLI, injecting the flagged patterns as context. A prompt that is a bare `;` means the user did not understand the previous reply, and the hook injects an instruction to restate it in plain English. Bare `aai-hook` resolves because the user's shell profile puts the workspace venv on PATH; if it does not, use the absolute venv path.

Desktop app: it has no launch flags, so no sysp replacement and no `claudedojo` launch. Hooks detect it (`CLAUDE_CODE_ENTRYPOINT` = `claude-desktop`): SessionStart prints `prompts/core.md` instead of the bootstrap gate, and native Write and Edit stay usable. NotebookEdit stays blocked everywhere: its writer saves non-ASCII as JSON escapes, churning whole notebooks. The bash guard runs in both frontends. Revisit if the desktop gains launch options.

Outcome, kernel-centric codex, in `~/.codex/hooks.json`: PostCompact, SessionStart with matcher `compact`, and PreToolUse with matcher `mcp__clikernel__execute` each run `<venv>/bin/aai-hook codex-orientation`; UserPromptSubmit runs `<venv>/bin/aai-hook codex-prompt-submit`. Hybrid codex does not install `codex-orientation`, since it does not run the dojo; it may still install `codex-prompt-submit`. codex asks the user to trust hooks on the first start after any `hooks.json` change; tell them to expect that prompt.

Check: `aai-hook claude-prompt-submit` fed `{"prompt": "test?"}` on stdin prints the question notice.
Expand All @@ -71,7 +73,7 @@ Outcome, in `settings.json`: `permissions.deny` includes `Read`, `Edit`, `Write`

Recommended, ask the user: `disableBundledSkills` set to `true` in `settings.json`, turning off the built-in skills (`init`, `review`, `code-review`, `security-review`, `simplify`, `verify`, `run`, `dataviz`, `artifact-design`, `fewer-permission-prompts`, `update-config`, `keybindings-help`), which assume the native file tools this deny list removes.

Settle first: any existing rule that conflicts. In particular a broad `Bash` allow rule defeats both the bash guard and safecmd; surface that one explicitly.
Settle first: any existing rule that conflicts. In particular a broad `Bash` allow rule defeats both the bash guard and safecmd; surface that one explicitly. Also whether the user works in the desktop app: settings cannot branch by frontend, and this deny list would strip desktop sessions too. Such users carry these rules in `~/.config/claudedojo/config.toml` instead (step 5).

Check: the file still parses as JSON after editing.

Expand All @@ -81,7 +83,16 @@ Outcome: symlinks from `~/.claude/skills/persistent-python` and `~/.claude/skill

safecmd auto-approves allowlisted Bash commands. The `safecmd` package is a workspace member, so it is already installed; its allowlist lives at `~/.config/safecmd/config.ini` and the defaults are fine to start.

Optional, Claude Code: the user might like `<this repo>/prompts/core.md` appended to the system prompt; a shell alias adding `--append-system-prompt-file <this repo>/prompts/core.md` to `claude` does it. The stronger option is the team's full behavioral prompt: symlink `~/.claude/sysp` to `<this repo>/prompts/sysp.md` and alias `claude` to `claude --system-prompt-file ~/.claude/sysp --append-system-prompt-file <this repo>/prompts/core.md`, which replaces Claude Code's default prompt entirely. Explain the trade to the user before wiring it: the default's tool schemas survive replacement, but its dynamic environment block and scratchpad path do not, and the behavioral text takes over from the default's guidance.
Optional, Claude Code: `claudedojo` launches `claude` on a session opening with the worked dojo round, adding the `claude_args` list from `~/.config/claudedojo/config.toml` (each `~`-expanded). That file carries the whole CLI launch: no shell alias, no `--settings` file:

claude_args = [
"--system-prompt-file", "~/.claude/sysp",
"--append-system-prompt-file", "<this repo>/prompts/core.md",
"--allowedTools", "WebSearch", "WebFetch", "mcp__clikernel__restart",
"--disallowedTools", "Read", "Edit", "Write", "Grep", "Glob", "NotebookEdit", "Bash(cat *)", "Bash(python -c:*)",
]

`--system-prompt-file` replaces Claude Code's default prompt with sysp.md (symlink `~/.claude/sysp` to `<this repo>/prompts/sysp.md`). Explain the trade before wiring it: the default's tool schemas survive replacement, but its dynamic environment block and scratchpad path do not.

Optional, codex: the analogue of the full behavioral prompt is `model_instructions_file = "<this repo>/prompts/codex-sysp.md"` (absolute path) in `~/.codex/config.toml`, replacing codex's built-in instructions entirely; `~/.codex/AGENTS.md` (and so `core.md`) still loads on top, and no symlink is involved since the key points straight into the checkout. Explain the trade to the user before wiring it: the file is the team's edited reconstruction of the built-in instructions, so upstream changes to codex's own prompt stop arriving until the file is revised.

Expand Down
18 changes: 15 additions & 3 deletions aai_coding/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,16 @@ def _forget(session_id, boundary=None):
except Exception: pass


def _desktop():
"True in a Claude desktop app session, which runs the relaxed harness: the desktop can neither replace the system prompt nor start dojo-preloaded"
return os.environ.get('CLAUDE_CODE_ENTRYPOINT') == 'claude-desktop'


CORE_MD = Path(__file__).parent.parent/'prompts'/'core.md'


def claude_session_start(o):
"SessionStart: orientation notice by source, then Python-project bootstrap and nbdev addenda"
"SessionStart: orientation notice by source, then Python-project bootstrap and nbdev addenda; the desktop app gets core.md instead of the bootstrap gate"
d = Path(os.environ.get('CLAUDE_PROJECT_DIR') or os.getcwd())
src = o.get('source', '')
if src in ('resume', 'compact'): print(f'[{src} at {datetime.now():%H:%M:%S}]')
Expand All @@ -92,7 +100,8 @@ def claude_session_start(o):
_forget(o.get('session_id', ''), bt)
print(SYNTH_MSG)
elif src == 'resume' and (d/'pyproject.toml').is_file(): print(RESUME_MSG)
if (d/'pyproject.toml').is_file(): print(BOOTSTRAP_MSG)
if _desktop(): print(CORE_MD.read_text())
elif (d/'pyproject.toml').is_file(): print(BOOTSTRAP_MSG)
try: nb = any(l.startswith('[tool.nbdev]') for l in (d/'pyproject.toml').open())
except OSError: nb = False
if nb: print(NBDEV_MSG)
Expand Down Expand Up @@ -122,7 +131,8 @@ def claude_bash_guard(o):


def claude_block_native_edit(o):
"PreToolUse(Write|Edit|NotebookEdit): route edits to the kernel tooling"
"PreToolUse(Write|Edit|NotebookEdit): route edits to the kernel tooling. Desktop sessions keep Write and Edit but never NotebookEdit: its writer saves non-ASCII as JSON escapes, churning every notebook it touches"
if _desktop() and o.get('tool_name') != 'NotebookEdit': return
print(BLOCK_EDIT_MSG, file=sys.stderr)
sys.exit(2)

Expand Down Expand Up @@ -285,6 +295,8 @@ def claude_slop(o):
if notes: print(json.dumps(dict(hookSpecificOutput=dict(
hookEventName='UserPromptSubmit', additionalContext='\n'.join(notes)))))
except Exception as e: print(f'[slop] fail-open: {e!r}', file=sys.stderr)


def codex_orientation(o):
"codex PostCompact/SessionStart/PreToolUse: post-compaction doc-state reset and one-shot reorientation"
state = Path(os.environ.get('LLMDOJO_STATE_DIR', Path.home()/'.local/state/llmdojo'))
Expand Down
6 changes: 3 additions & 3 deletions plugins/safecmd/scripts/validator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""Claude Code PreToolUse hook that auto-approves safe bash commands using safecmd."""
import sys,json
import sys,json,os

def respond(decision, reason):
print(json.dumps({"hookSpecificOutput": {
Expand All @@ -11,7 +11,7 @@ def respond(decision, reason):
sys.exit(0)

try: from safecmd import validate, DisallowedError
except ImportError as e: respond("defer", f"safecmd import failed: {e}")
except ImportError: sys.exit(0) # no opinion: exit 0 with no output falls through to the normal permission flow

def main():
try: hook_input = json.load(sys.stdin)
Expand All @@ -24,6 +24,6 @@ def main():
validate(cmd)
respond("allow", "safecmd: validated")
except DisallowedError:
respond("defer", "Not on safecmd allowlist")
sys.exit(0) # off-allowlist: no opinion, normal permission flow ("defer" means resume-later and ends the run in the desktop app)

if __name__ == "__main__": main()
6 changes: 6 additions & 0 deletions scripts/clikernel-mcp-shim
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh
# Runs clikernel-mcp, adding --quiet in the desktop app: desktop sessions take
# their instructions from the hooks, not from the kernel's startup notice.
mcp="$(CDPATH= cd "$(dirname "$0")/../.." && pwd)/.venv/bin/clikernel-mcp"
[ "$CLAUDE_CODE_ENTRYPOINT" = claude-desktop ] && exec "$mcp" --quiet "$@"
exec "$mcp" "$@"
19 changes: 19 additions & 0 deletions tests/test_harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ def out(): return capsys.readouterr().out
assert out() == '' # unparseable transcript: fail-open, silent on stdout


def test_desktop_relaxed(tmp_path, monkeypatch, capsys):
"Desktop sessions keep native Write/Edit (never NotebookEdit) and get core.md instead of the bootstrap gate; terminal sessions enforce both"
from aai_coding.harness import claude_block_native_edit, claude_session_start
monkeypatch.setenv('CLAUDE_PROJECT_DIR', str(tmp_path))
(tmp_path/'pyproject.toml').write_text('')
monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'claude-desktop')
claude_block_native_edit(dict(tool_name='Edit'))
with pytest.raises(SystemExit): claude_block_native_edit(dict(tool_name='NotebookEdit')) # its writer's escape churn corrupts notebooks
claude_session_start(dict(source='startup', session_id='s1'))
out = capsys.readouterr().out
assert 'final text message' in out and 'NEVER touch local files' not in out
monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'cli')
with pytest.raises(SystemExit): claude_block_native_edit(dict(tool_name='Edit'))
claude_session_start(dict(source='startup', session_id='s1'))
out = capsys.readouterr().out
assert 'NEVER touch local files' in out and 'final text message' not in out


@pytest.mark.skipif(not which('slopometer'), reason='slopometer not installed')
def test_slop(tmp_path, monkeypatch, capsys):
"Sloppy previous message -> context rows at the next prompt; repeats, subagents, short and clean prose stay silent"
Expand Down Expand Up @@ -200,6 +218,7 @@ def out(): return capsys.readouterr().out

def test_synthetic_wipe_guard(tmp_path, monkeypatch):
import llmdojo.rules as lr
monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'cli') # the wipe guard lives on the terminal path; don't inherit a desktop env
monkeypatch.setenv('LLMDOJO_STATE_DIR', str(tmp_path/'state'))
monkeypatch.setenv('CLAUDE_CODE_SESSION_ID', 'tsid')
monkeypatch.delenv('CODEX_THREAD_ID', raising=False)
Expand Down