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
19 changes: 19 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "marginal",
"owner": {
"name": "SignalLayer Labs",
"url": "https://github.com/SignalLayerLabs/Marginal"
},
"metadata": {
"description": "Compute governance that has to justify its own cost.",
"version": "0.3.3"
},
"plugins": [
{
"name": "marginal-claude-code",
"source": "./plugins/marginal-claude-code",
"description": "Local-first compute governance evidence for Claude Code, in Shadow Mode.",
"category": "productivity"
}
]
}
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to MARGINAL are documented here. The project follows Semanti

## [Unreleased]

### Added

- a Claude Code plugin labeled **Observe**: it records normalized tool-call evidence and
repeated-work recommendations in a local Decision Ledger, declares no control capability, and never
blocks a tool call or returns hook output;
- `marginal.integrations.hookkit`, the engine-independent parts of a hook integration: normalized
events, privacy-safe action normalization, conservative structured outcome classification,
workspace state evidence that fails open, and session correlation;
- `marginal install claude-code` and `marginal uninstall claude-code`.

### Changed

- the authenticated loopback session transport moved from `marginal.integrations.codex.transport` to
`marginal.integrations.transport` so every hook adapter shares it. The old import path re-exports
it unchanged.

## [0.3.3] - 2026-08-13

### Fixed
Expand Down
97 changes: 97 additions & 0 deletions docs/integrations/claude-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Claude Code Plugin

Capability label: **Observe**.

The plugin records normalized tool-call evidence and repeated-work recommendations in a local
Decision Ledger. It never blocks a tool call, never rewrites tool arguments, and never returns
output to Claude Code. Shadow Mode is the only mode this integration supports today.

Validated against Claude Code 2.1.233 on Linux.

## Install

```bash
claude plugin marketplace add SignalLayerLabs/Marginal
claude plugin install marginal-claude-code@marginal
```

`marginal install claude-code` runs the same two commands through the Claude Code CLI.

The plugin needs the `marginal-ai` package importable by the `python3` on your `PATH`:

```bash
python3 -m pip install --user marginal-ai
```

If `marginal` cannot be imported, every hook exits 0 without output and Claude Code behaves exactly
as if the plugin were absent. The plugin never installs anything on your behalf.

Point `MARGINAL_RUNTIME` at a directory or zipapp to load MARGINAL from somewhere else.

## Remove

```bash
claude plugin uninstall marginal-claude-code@marginal
```

Removal leaves the ledger in place. Delete the plugin data directory to discard the evidence.

## What it observes

| Hook | What MARGINAL records |
|---|---|
| `SessionStart` | starts one authenticated loopback service for the session |
| `PreToolUse` | a normalized action, its semantic key, workspace state hash, and the repetition signal |
| `PostToolUse` | a **proven success** with engine-measured `duration_ms` |
| `PostToolUseFailure` | a **proven failure**, or `unknown` when the call was interrupted |
| `SessionEnd` | a session summary, then closes the service |

Claude Code separates success from failure at the event level, so outcome is an engine-declared fact
rather than something inferred from response text. That is a stronger evidence surface than a single
completion hook provides. Two limits still apply:

- per-tool token usage is not exposed, so token cost is reported as unavailable rather than as zero;
- a hook that Claude Code does not deliver leaves a proposal unmatched, and the session records it as
`unknown` instead of assuming success.

## Repeated work

A repeat is only interesting when the same semantic action runs against the same workspace state and
produces the same completion evidence. The ledger escalates through
`NO_PROGRESS_OBSERVED` to `NO_PROGRESS_ENFORCEMENT_ELIGIBLE`, and Shadow Mode still allows the
action, recording `SHADOW_OVERRIDE`. Nothing is blocked.

Read the recommendations back with the standard tooling:

```bash
marginal ledger-report "$CLAUDE_PLUGIN_DATA/ledger"/*/*.jsonl
```

## Enforcement

This integration does not enforce. The adapter declares no control capability, so the core refuses to
run it in a blocking mode. The documented deny transport is implemented and tested
(`marginal.integrations.claude_code.decisions`) but nothing calls it.

Turning it on requires an evidence gate equivalent to the Codex Earned Enforcement window: proven hook
coverage, reviewed stop candidates, zero false stops, and bounded governance latency. Until that
evidence exists for Claude Code, a stop recommendation stays a recommendation.

## Privacy and limits

The ledger is written under the plugin's own data directory with owner-only permissions.

- tool arguments, command text, tool output, prompts, and transcripts are never persisted; only
digests of them are;
- error text from a failed tool contributes to an evidence digest and is not written to the ledger;
- `LOCAL_FULL` is the default profile because the ledger stays on the user's machine. It retains
session identifiers and tool names;
- set `MARGINAL_PRIVACY_PROFILE=safe_telemetry` to write keyed pseudonyms instead. Pseudonymization is
not anonymization;
- Claude Code's own transcripts, shell history, and telemetry are outside this boundary.

## Workspace state

State evidence comes from Git. A session directory inside a work tree is observable, including a
repository with no commits yet. Outside a repository the state hash is empty, which makes every
repetition control fail open rather than invent certainty.
12 changes: 11 additions & 1 deletion docs/integrations/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ It provides lifecycle correlation, privacy-safe normalization, outcome classific
authenticated local service, Shadow Mode, Earned Enforcement receipts, and reversible native
installation. See [Codex plugin](codex.md).

OpenCode, Claude Code, and GitHub Copilot remain roadmap work. Codex is labeled Tool Enforcement,
The Claude Code plugin is labeled **Observe**. It records normalized evidence and repeated-work
recommendations in a local Decision Ledger, declares no control capability, and never blocks a tool
call. Its outcome evidence is engine-declared, because Claude Code reports success and failure as
separate hook events. See [Claude Code plugin](claude-code.md).

OpenCode and GitHub Copilot remain roadmap work. Codex is labeled Tool Enforcement,
not Full Compute Enforcement, because specialized and hosted tool paths can fall outside local
hook coverage.

Engine-independent parts of a hook integration live in `marginal.integrations.hookkit`: normalized
events, action normalization, structured outcome classification, workspace state evidence, and
session correlation. The Codex integration predates that module and still carries its own copies;
migrating it is separate work so a new adapter never destabilizes the validated Codex path.
18 changes: 18 additions & 0 deletions plugins/marginal-claude-code/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "marginal-claude-code",
"version": "0.3.3",
"description": "Local-first compute governance evidence for Claude Code. Shadow Mode only: it observes repeated work and never blocks a tool call.",
"author": {
"name": "SignalLayer Labs",
"url": "https://github.com/SignalLayerLabs/Marginal"
},
"homepage": "https://signallayerlabs.github.io/Marginal/",
"repository": "https://github.com/SignalLayerLabs/Marginal",
"license": "Apache-2.0",
"keywords": [
"agent-compute",
"compute-governance",
"claude-code",
"token-efficiency"
]
}
68 changes: 68 additions & 0 deletions plugins/marginal-claude-code/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"description": "Local-only MARGINAL Shadow Mode observation for Claude Code.",
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/marginal_hook.py\"",
"timeout": 10,
"statusMessage": "Starting MARGINAL Shadow Mode"
}
]
}
],
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/marginal_hook.py\"",
"timeout": 5,
"statusMessage": "Measuring marginal value"
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/marginal_hook.py\"",
"timeout": 5,
"statusMessage": "Recording redacted completion evidence"
}
]
}
],
"PostToolUseFailure": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/marginal_hook.py\"",
"timeout": 5,
"statusMessage": "Recording redacted failure evidence"
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/scripts/marginal_hook.py\"",
"timeout": 5,
"statusMessage": "Closing MARGINAL session"
}
]
}
]
}
}
52 changes: 52 additions & 0 deletions plugins/marginal-claude-code/scripts/marginal_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Dependency-free launcher for the MARGINAL Claude Code hook.

The launcher never installs anything and never fails a hook. If the ``marginal``
package cannot be imported, it exits 0 with no output and Claude Code proceeds
exactly as if MARGINAL were not present.

Resolution order:

1. ``MARGINAL_RUNTIME`` — a directory or zipapp added to ``sys.path``;
2. ``runtime/marginal_runtime.pyz`` bundled next to this script, when present;
3. the ambient interpreter's own ``marginal`` installation.
"""

from __future__ import annotations

import os
import sys
from pathlib import Path


def _candidate_paths() -> list[str]:
candidates: list[str] = []
override = os.environ.get("MARGINAL_RUNTIME")
if override:
candidates.append(override)
plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT")
root = Path(plugin_root) if plugin_root else Path(__file__).resolve().parent.parent
bundled = root / "runtime" / "marginal_runtime.pyz"
if bundled.is_file():
candidates.append(str(bundled))
return candidates


def main() -> int:
if sys.version_info < (3, 10): # noqa: UP036 - plugin bootstrap must fail open on unsupported Python
return 0
for candidate in _candidate_paths():
if candidate not in sys.path:
sys.path.insert(0, candidate)
try:
from marginal.integrations.claude_code.service import hook_main
except Exception:
return 0
try:
return hook_main([])
except Exception:
return 0


if __name__ == "__main__":
raise SystemExit(main())
Binary file modified plugins/marginal/runtime/marginal_runtime.pyz
Binary file not shown.
2 changes: 1 addition & 1 deletion plugins/marginal/runtime/provenance.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"builder":"scripts/build_codex_plugin.py","python_requires":">=3.10","schema_version":1,"sha256":"57b0f86a9c49daff2c0213d5606d2c369c4e39580b52cd76cfd20fdb2de33167","source_hash":"fccff1138073eb96d48fbb60470bb7f4d6d27770da7a2f61bc490b83ee3a912a"}
{"builder":"scripts/build_codex_plugin.py","python_requires":">=3.10","schema_version":1,"sha256":"97a3774d21d4fa2e461cfabe0401ecfd474274fd9550534152cd6a44e5f3945a","source_hash":"e4f8f0486b6d71a20c3e53db928692918332c38789b0a88c4ff004e3f7ea90b5"}
31 changes: 29 additions & 2 deletions src/marginal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,15 @@ def _build_parser() -> argparse.ArgumentParser:
public_eval.add_argument("--seed", type=int, default=42)

install_parser = subparsers.add_parser("install", help="install a native integration")
install_parser.add_argument("target", choices=["codex"])
install_parser.add_argument("target", choices=["codex", "claude-code"])
install_parser.add_argument("--repository", default="SignalLayerLabs/Marginal")
install_parser.add_argument("--ref", default="main")
install_parser.add_argument("--data-dir", type=Path)
install_parser.add_argument("--autopilot-consent", action="store_true")
install_parser.add_argument("--json", action="store_true", dest="as_json")

uninstall_parser = subparsers.add_parser("uninstall", help="remove a native integration")
uninstall_parser.add_argument("target", choices=["codex"])
uninstall_parser.add_argument("target", choices=["codex", "claude-code"])
uninstall_parser.add_argument("--purge-data", action="store_true")
uninstall_parser.add_argument("--yes", action="store_true")
uninstall_parser.add_argument("--data-dir", type=Path)
Expand Down Expand Up @@ -201,6 +201,23 @@ def main(argv: Sequence[str] | None = None) -> int:
args = parser.parse_args(argv)

if args.command == "install":
if args.target == "claude-code":
from .integrations.claude_code.installer import (
MARKETPLACE_SOURCE,
)
from .integrations.claude_code.installer import (
install as install_claude_code,
)

claude_result = install_claude_code(
marketplace_source=args.repository or MARKETPLACE_SOURCE
)
if args.as_json:
print(json.dumps(claude_result.to_dict(), sort_keys=True))
else:
print(claude_result.message or claude_result.error_code or claude_result.selector)
return 0 if claude_result.installed else 1

from .integrations.codex.installer import install

result = install(
Expand All @@ -217,6 +234,16 @@ def main(argv: Sequence[str] | None = None) -> int:
return 0 if result.installed else 1

if args.command == "uninstall":
if args.target == "claude-code":
from .integrations.claude_code.installer import uninstall as uninstall_claude_code

claude_result = uninstall_claude_code()
if args.as_json:
print(json.dumps(claude_result.to_dict(), sort_keys=True))
else:
print(claude_result.message or claude_result.error_code or claude_result.selector)
return 0 if not claude_result.installed else 1

from .integrations.codex.commands import default_data_dir, purge_data
from .integrations.codex.installer import uninstall

Expand Down
Loading