magus v0.4.2 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
6 min read

Claude Code

Claude Code reads Agent Skills from .claude/skills/ and runs a PreToolUse hook before every tool call. That covers both guard surfaces, and both verdicts reach the model, so nothing in the contract is lost here. It is also the setup this repository dogfoods and the only one executed end to end against a real event.

what where
skills .claude/skills/
guard wiring .claude/settings.json, PreToolUse
command surface deny and advise both reach the model
file surface deny and advise both reach the model
MCP MCP
attention events Notification, Stop, SubagentStop
lease PreToolUse on the sub-agent tool

Skills

magus agent install .claude/skills

Commit what it writes so every teammate's agent gets the same instructions. Claude Code discovers skills when a session starts, so restart the session before it can invoke anything new. Skills covers the install surface, the two permutations, and the drift check.

MCP

magus server start

The daemon serves MCP on http://127.0.0.1:7391/mcp; MCP has the token and client setup. Tools are discovered at launch, so a client already running when the daemon comes up sees them only after a restart.

Guard hook

Two PreToolUse entries: one matching Bash for the command rules, one matching the file-editing tools for the declared-output and notes rules. Both run a template you own - download them from Guard hook templates and point the config at your copies.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "sh ~/.claude/hooks/magus-guard-command.sh", "timeout": 10 }]
      },
      {
        "matcher": "Edit|Write|NotebookEdit",
        "hooks": [{ "type": "command", "command": "sh ~/.claude/hooks/magus-guard-path.sh", "timeout": 10 }]
      }
    ]
  }
}

This repository's own .claude/settings.json points at the templates in docs/guides/integrations/agents/ rather than at a private copy, and a test fails if it stops doing so. What magus dogfoods is what you download.

magus session hook also reads Claude Code's event JSON directly: tool_input.command, tool_input.file_path, session_id and hook_event_name are the fields it knows, and a payload carrying a file path is judged as a write without --path. So one command serves both matchers, with no jq and no script:

magus session hook -o 'template={{if eq .decision "deny"}}{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":{{toJson .reason}}}}{{else if eq .decision "advise"}}{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":{{toJson .context}}}}{{end}}'

What it trades away is the templates' handling of a magus that is missing or too old to judge: both of those render nothing, and Claude Code reads nothing as allow, so the session goes unguarded with no sign of it. Use the short form while you are experimenting; use the templates once you rely on the guard.

Lease capture

When Claude Code hands work to a sub-agent it does so through a tool call, and that call fires PreToolUse like any other - carrying the whole prompt the orchestrator is handing over in tool_input.prompt. Neither guard matcher above selects it, so by default magus never sees a lease. Add a third entry to record one:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Task",
        "hooks": [
          {
            "type": "command",
            "command": "d=$PWD; while [ -n \"$d\" ] && [ ! -f \"$d/magusfile.buzz\" ]; do d=${d%/*}; done; GUARD_MAGUS_BIN=$([ -x \"$d/magus\" ] && printf %s \"$d/magus\" || command -v magus 2>/dev/null); [ -n \"$GUARD_MAGUS_BIN\" ] && \"$GUARD_MAGUS_BIN\" session hook --agent-name claude-code >/dev/null 2>&1; exit 0",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

No jq and no template: the whole event goes in unchanged, and magus reads tool_input.prompt for the context, tool_input.subagent_type (then description, then tool_name) for the callee's label, and session_id for the parent's session. The result is one agent_spawn event per lease, with the handed context stored as a payload blob you fetch by ref.

The while loop at the front is how it finds magus, and it is doing the same job as the templates' longer version: walk up from the hook's working directory to the nearest magusfile.buzz, prefer that workspace's own ./magus, and fall back to PATH. A hook runs in the SESSION's directory, which is not always the workspace root - open a session one level down and a plain ./magus is not there. It falls through to PATH silently, and where the PATH copy cannot load the workspace, the event is simply never recorded. Nothing surfaces that: an audit trail with holes reads exactly like one nobody wrote to.

It records; it does not judge. A lease prompt is prose, so the command rules never run against it and the verdict is always a pass - a prompt that mentions a denied command describes it rather than runs it. Output is discarded and the exit status is forced to 0 for the same reason the notification hook does it: an audit step must not be able to break the session it observes.

To join those events to a ledger, write the marker line documented in Any other host at the top of the prompt you hand the sub-agent.

Notifications

magus session notify turns a host event into a desktop notification. It does not send an event to the daemon or Console. Wire Notification (it fires on a permission prompt and when the agent goes idle waiting for input), and Stop or SubagentStop for completion.

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "d=$PWD; while [ -n \"$d\" ] && [ ! -f \"$d/magusfile.buzz\" ]; do d=${d%/*}; done; GUARD_MAGUS_BIN=$([ -x \"$d/magus\" ] && printf %s \"$d/magus\" || command -v magus 2>/dev/null); [ -n \"$GUARD_MAGUS_BIN\" ] && jq -c '{schema_version: 1, outcome: .hook_event_name, source: {kind: \"agent\"}, message: .message}' | \"$GUARD_MAGUS_BIN\" session notify --desktop >/dev/null 2>&1; exit 0"
          }
        ]
      }
    ]
  }
}

It exits 0 and swallows its own output on purpose: a notifier that can fail is a hook that can break the session it was meant to watch. It opens with the same magusfile walk as the lease hook above, for the same reason. Attention hooks covers the envelope and the outcome vocabulary.

Coverage and limits

No gaps. Both guard surfaces are wired, deny arrives as a permissionDecision, and advise arrives as additionalContext, which is the only channel that puts an explanation in front of the model rather than the person.

Verify

magus doctor

doctor's guard binary check names the binary a hook would actually run and fails when it is older than your working tree; guard wiring runs a canary command through it and then looks for a host config that invokes a current template; agent skills grades the installed copies against the running binary and --fix reinstalls whatever it reports stale.

Commit .claude/settings.json once you are happy with it. Until a checkout has that file, its guard rules are correct and entirely unenforced, with nothing in the session saying so - which is the gap the guard wiring check exists to report.

agentsclaudeclaude codeskillsguardhooksnotifications
Last updated (4f8cc295)
Earlier changes on this page (6)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). See operations.

Buzz

The language magusfiles are written in (the .buzz engine). See engines.

Daemon

The background magus host that owns shared state such as services and the warm knowledge graph. See daemon.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Session

One magus process's recorded facts - the targets it finished, their outcomes, and the lease it acted as - kept in a repo-scoped store every worktree shares. magus session lists them; the store prunes itself by last-fact age.

Lease

One row of the lease ledger: a piece of work an orchestrating agent handed out, with its goal, the checkpoint it was cut against, and the paths it owns or must not touch. The ledger records; the agent guard is what reads those facts back when grading a write. See doctrine.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.