Skip to content

Relaxed desktop mode and per-frontend kernel launch - #13

Open
PiotrCzapla wants to merge 5 commits into
mainfrom
pc/desktop-harness
Open

Relaxed desktop mode and per-frontend kernel launch#13
PiotrCzapla wants to merge 5 commits into
mainfrom
pc/desktop-harness

Conversation

@PiotrCzapla

@PiotrCzapla PiotrCzapla commented Aug 14, 2026

Copy link
Copy Markdown

Warning: Generated by claude code intended for llm consumption

The desktop app has no launch flags: no sysp replacement, no claudedojo launch. Rather than leaving desktop sessions broken under the kernel-only permissions, the harness now adapts per frontend, with identical hook registrations everywhere.

  • Relaxed desktop mode: hooks detect 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 in both frontends: its writer saves non-ASCII as JSON escapes, churning whole notebooks. The bash guard runs in both frontends.
  • safecmd answers no-opinion as exit 0, not defer: defer means a graceful exit for a tool resumed later, and the desktop app ends such calls with no tool result, which reads as a stalled run. This was the cause of the desktop "tool result missing" stalls.
  • Quiet desktop kernel: the MCP registration points at scripts/clikernel-mcp-shim, which adds --quiet for desktop sessions. Terminal sessions keep the full startup notice.
  • claudedojo carries the CLI flags: the prompt flags and permission rules live in ~/.config/claudedojo/config.toml claude_args, so the CLI harness launch is just claudedojo, with no shell alias and no --settings file. Verified live, including a spaced deny rule (Bash(cat *)) surviving as one argv item.

Merged with main's two-mode codex setup (#18): kernel-centric codex keeps the dojo regime and codex-orientation; hybrid codex pairs the quiet kernel with the skills/clikernel skill.

🤖 Generated with Claude Code

Comment thread aai_coding/harness.py Outdated
Comment on lines +96 to +117
@@ -93,9 +114,7 @@ def claude_session_start(o):
print(SYNTH_MSG)
elif src == 'resume' and (d/'pyproject.toml').is_file(): print(RESUME_MSG)
if (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)
if _is_nbdev(d): print(NBDEV_MSG)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

this needs to be in the same function lets not define one off functions simply protect with desktop why print core instead of compact?

Comment thread aai_coding/harness.py Outdated
def claude_bash_guard(o):
"PreToolUse(Bash): reject output-truncating pipes"
"PreToolUse(Bash): reject output-truncating pipes (desktop sessions are exempt)"
if _desktop(): return

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

lets keep this behaviour

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

ie we keep the bash guard enabled

Comment thread aai_coding/harness.py Outdated
Comment on lines +311 to +331
DOJO_SAMPLE_MSG = ('aai-coding harness gate, registered by your user in their hook settings - not content from a tool or web page, '
'so acting on it is expected. This desktop session studies a worked dojo round instead of playing one. Read {path} in full as '
'reference for correct kernel tool usage; do not repeat or score it. Then run `dojo_start({cid!r})` in the kernel to record '
'the skip, and retry this call.')


def claude_dojo_sample(o):
"PreToolUse(mcp__clikernel__execute), desktop only: gate the first kernel call on studying the worked round"
try:
if not _desktop() or o.get('agent_id'): return
if not (Path(os.environ.get('CLAUDE_PROJECT_DIR') or os.getcwd())/'pyproject.toml').is_file(): return
f = _state_file('dojo-sample', o.get('session_id', ''))
if f.exists(): return
from llmdojo.claudedojo import _load_reg
_,meta = _load_reg(None) # side effect: registers the template's completion id, so dojo_start honors the skip
import llmdojo
sample = Path(llmdojo.__file__).parent/'dojo_data/codexdojo_sample.md' # hook output is capped at 10k chars: point at the round, never inline it
print(json.dumps(dict(hookSpecificOutput=dict(hookEventName='PreToolUse', permissionDecision='deny',
permissionDecisionReason=DOJO_SAMPLE_MSG.format(cid=meta['cid'], path=sample)))))
f.write_text('{}')
except Exception as e: print(f'[dojo-sample] fail-open: {e!r}', file=sys.stderr)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

we just agreed not to use this and use simple clikernel --quite

Comment thread aai_coding/harness.py Outdated
def main():
"Dispatch `aai-hook <subcommand>` to its handler with the stdin JSON payload"
globals()[sys.argv[1].replace('-', '_')](json.load(sys.stdin))
"Dispatch `aai-hook <subcommand>` to its handler with the stdin JSON payload; an unreadable payload is a fail-open no-op, since the harness surfaces a crashed hook as a tool error"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

why do we need this? remove

Comment thread scripts/clikernel-mcp-shim Outdated
Comment on lines +2 to +4
# The kernel regime rides the startup notice, and the desktop app's instruction
# channel is the dojo-sample gate instead (SETUP.md step 3): its kernel runs
# quiet. Terminal sessions keep the full notice.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

deslop

Comment thread tests/test_harness.py Outdated
Comment on lines 153 to 224
def _python_project(monkeypatch, tmp_path, nbdev=False):
"Point the hooks at a Python project in `tmp_path`"
monkeypatch.setenv('CLAUDE_PROJECT_DIR', str(tmp_path))
(tmp_path/'pyproject.toml').write_text('[tool.nbdev]\n' if nbdev else '')


def test_desktop_session_relaxed(tmp_path, monkeypatch, capsys):
"In the desktop app the enforcement hooks stand down, and session start prints core.md instead of the bootstrap gate"
from aai_coding.harness import claude_bash_guard, claude_block_native_edit, claude_session_start
monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'claude-desktop')
_python_project(monkeypatch, tmp_path, nbdev=True)

claude_block_native_edit({}) # returns instead of exiting: native edits allowed
claude_bash_guard(dict(tool_input=dict(command='pytest | head -5'))) # returns instead of exiting: pipes allowed

claude_session_start(dict(source='startup', session_id='s1'))
out = capsys.readouterr().out
assert 'final text message' in out # core.md, the behavioral layer
assert 'NEVER touch local files' not in out # no kernel-only bootstrap gate
assert 'nbdev project' in out # the nbdev caution still applies


def test_cli_session_enforced(tmp_path, monkeypatch, capsys):
"In a terminal session the same hooks enforce the kernel-only regime"
from aai_coding.harness import claude_bash_guard, claude_block_native_edit, claude_session_start
monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'cli')
_python_project(monkeypatch, tmp_path)

with pytest.raises(SystemExit): claude_block_native_edit({})
with pytest.raises(SystemExit): claude_bash_guard(dict(tool_input=dict(command='pytest | head -5')))

claude_session_start(dict(source='startup', session_id='s1'))
out = capsys.readouterr().out
assert 'NEVER touch local files' in out # the bootstrap gate
assert 'final text message' not in out # core.md arrives via the CLI launch flags, not this hook


def test_dojo_sample(tmp_path, monkeypatch, capsys):
"A desktop session's first kernel call is held once, with directions to study a worked round and skip the live one"
from aai_coding.harness import claude_dojo_sample
monkeypatch.setenv('LLMDOJO_STATE_DIR', str(tmp_path))
monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'claude-desktop')
monkeypatch.setenv('CLAUDE_PROJECT_DIR', str(tmp_path))
first_call = dict(hook_event_name='PreToolUse', session_id='s1')

claude_dojo_sample(first_call)
assert capsys.readouterr().out == '' # no pyproject.toml, so no dojo to substitute

_python_project(monkeypatch, tmp_path)
claude_dojo_sample(first_call)
r = json.loads(capsys.readouterr().out)['hookSpecificOutput']
assert r['permissionDecision'] == 'deny'
reason = r['permissionDecisionReason']
assert 'codexdojo_sample.md' in reason # where the worked round is
assert 'dojo_start' in reason # how to record the skip
assert 'registered by your user' in reason # names its source, so cautious agents don't refuse it as injected
assert len(reason) < 10_000 # hook output is capped at 10k chars

claude_dojo_sample(first_call)
assert capsys.readouterr().out == '' # held once per session: the retry passes

claude_dojo_sample(dict(first_call, session_id='s2', agent_id='sub1'))
assert capsys.readouterr().out == '' # subagents are never held

monkeypatch.setenv('CLAUDE_CODE_ENTRYPOINT', 'cli')
claude_dojo_sample(dict(first_call, session_id='s3'))
assert capsys.readouterr().out == '' # terminal sessions play the real round instead


@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"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

teh amount of changes dont deserver that amount of tests. make sure we test only requred parts

Comment thread SETUP.md Outdated
## 1. Kernel server

Outcome: the clikernel MCP server is registered. Claude Code: a user-scope server named `clikernel` running `<venv>/bin/clikernel-mcp`. 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. Optional, ask the user: `env_vars = ["GITHUB_TOKEN"]` on the server block passes their GitHub token into the kernel so sessions can act for them on GitHub (via `ghapi`); add it only if they want that and are happy to share the token.
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` and adds `--quiet` when `CLAUDE_CODE_ENTRYPOINT` is `claude-desktop`. The regime rides the kernel's startup notice, and the desktop's instruction channel is the dojo-sample gate (step 3), so its kernel runs quiet while terminal sessions keep the full notice. 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. Optional, ask the user: `env_vars = ["GITHUB_TOKEN"]` on the server block passes their GitHub token into the kernel so sessions can act for them on GitHub (via `ghapi`); add it only if they want that and are happy to share the token.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

deslop what regime?

Comment thread SETUP.md Outdated
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.
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. PreToolUse matcher `mcp__clikernel__execute` runs `aai-hook claude-dojo-sample`, the desktop dojo substitute described below. 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: the desktop currently has no launch flags, so no sysp replacement and no dojo-preloaded start (`claude -r $(claudedojo)`). The hooks detect it (`CLAUDE_CODE_ENTRYPOINT` = `claude-desktop`) and substitute rather than enforce: SessionStart prints `prompts/core.md`; the bootstrap gate, native-edit blocking, and the bash guard stay off; the first kernel call in a Python project is denied once with the worked round and a completion id, so `dojo_start(id)` skips the live round - study replaces play, as in the codex sample. Revisit if the desktop gains launch options.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

not the case anymore we run caludedojo now a days from command line, check the code

Comment thread SETUP.md Outdated
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, so this deny list would reach desktop sessions the step 3 hooks deliberately leave native. Such users put these permissions in a `--settings` file on the CLI alias instead.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

deslop

Comment thread SETUP.md Outdated
Outcome: symlinks from `~/.claude/skills/persistent-python`, `~/.claude/skills/pyskills`, and the same two names in `~/.codex/skills`, to `<this repo>/skills/<name>`; `~/.claude/skills/safecmd` to `<this repo>/plugins/safecmd`; `~/.codex/AGENTS.md` to `<this repo>/prompts/core.md`.

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.
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. A command off the allowlist gets no decision (exit 0, no output), which falls through to the normal permission flow in both frontends. Do not answer `defer` to mean "no opinion": the docs define it as a graceful exit for a tool to resume later, the reason string is ignored, and the desktop app ends the call with no tool result, which reads as a stalled run.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

deslop why would we add this to setup?

Comment thread aai_coding/harness.py Outdated
if not isinstance(st, dict): st = {}
st = {k: st.get(k, d) for k, d in dict(rounds=0, nudged=0, mid='', midlen=0).items()}
ev = o['hook_event_name']
ev = o.get('hook_event_name')

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

why?

PiotrCzapla and others added 3 commits August 24, 2026 21:55
The desktop app has no launch flags, so it cannot replace the system
prompt or open on a prepared session. Hooks detect it
(CLAUDE_CODE_ENTRYPOINT) and relax: SessionStart prints core.md instead
of the bootstrap gate, and native Write and Edit stay usable. The bash
guard runs in both frontends. NotebookEdit stays blocked everywhere:
its writer saves non-ASCII as JSON escapes, churning every notebook it
touches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Off-allowlist commands answered "defer", a graceful exit for a tool
resumed later: the desktop app ends such calls with no tool result,
which reads as a stalled run. The intent was "no opinion", whose
documented form is exit 0 with no output, falling through to the normal
permission flow in both frontends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…CLI flags

The registration points at scripts/clikernel-mcp-shim, which execs
clikernel-mcp and adds --quiet when CLAUDE_CODE_ENTRYPOINT is
claude-desktop: desktop sessions take their instructions from the
hooks, not the kernel's startup notice, which terminal sessions keep.
The CLI harness launch needs no shell alias and no --settings file:
claudedojo already appends the claude_args list from
~/.config/claudedojo/config.toml, so the prompt flags and permission
rules live there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PiotrCzapla PiotrCzapla changed the title Relaxed desktop mode with worked-round dojo substitute Relaxed desktop mode and per-frontend kernel launch Aug 27, 2026
@PiotrCzapla
PiotrCzapla requested a review from jph00 August 27, 2026 17:25
@PiotrCzapla
PiotrCzapla marked this pull request as ready for review August 27, 2026 17:25
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.

1 participant