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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

### Added
- Added `agentguard init --agent auto` to detect installed agent directories and initialize each supported agent in order while continuing after per-agent failures.
- Added automatic Hermes hook configuration and QClaw plugin enablement during `agentguard init --agent` and `setup.sh` installs.

### Changed
- `agentguard init` now stores all initialized agent hosts in config while keeping the first detected host as the default for `--cron-target auto`.
- `agentguard init --agent codex` now writes `.codex/agentguard-hook.json` as the concrete AgentGuard hook configuration file instead of an example filename.
- Install guidance now treats `agentguard init --agent auto` as the only required next step; Cloud connect and checkup remain optional commands.
- Postinstall now writes persistent next-step guidance to `~/.agentguard/next-steps.txt` and the package directory so agent installers can discover it even when npm hides lifecycle output.

### Fixed
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ AI coding agents can execute any command, read any file, and install any skill

```bash
npm install -g @goplus/agentguard
agentguard init
agentguard init --agent auto
agentguard status
```

The npm install runs a best-effort local bootstrap; `agentguard init` ensures `~/.agentguard/config.json` exists and protects locally by default.
The npm install runs a best-effort local bootstrap; `agentguard init --agent auto` is the required next step that detects installed agent directories and configures supported hooks/plugins.
No Cloud account or network connection is required for the local runtime guard.

## 3 minutes: protect your agent
Expand Down Expand Up @@ -107,8 +107,7 @@ agentguard subscribe --json
# Or run a one-off self-check against a single advisory id
agentguard checkup --against-advisory AGS-2026-0042

# Optional: write host-specific hook templates.
# OpenClaw also installs and enables the AgentGuard plugin.
# Re-run host setup manually when needed. `auto` detects installed agents.
agentguard init --agent auto
agentguard init --agent claude-code
agentguard init --agent codex
Expand Down
2 changes: 1 addition & 1 deletion docs/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ To write Codex templates in the current project:
agentguard init --agent codex
```

This creates `.codex/skills/agentguard/SKILL.md` and `.codex/agentguard-hook.example.json`.
This creates `.codex/skills/agentguard/SKILL.md` and `.codex/agentguard-hook.json`.

Pipe a tool event to `agentguard protect`:

Expand Down
6 changes: 3 additions & 3 deletions docs/hermes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Build AgentGuard first so the hook script can import `dist/index.js`:
npm run build
```

Copy the template from `skills/agentguard/hermes-hooks.yaml` into
`~/.hermes/config.yaml` and replace `AGENTGUARD_SKILL_DIR` with the absolute
path to the installed AgentGuard skill directory.
Run `agentguard init --agent hermes` to install the skill and merge the
AgentGuard hook entries into `~/.hermes/config.yaml`. The bundled template at
`skills/agentguard/hermes-hooks.yaml` is still available for manual setups.

```yaml
hooks:
Expand Down
183 changes: 182 additions & 1 deletion setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ MIN_NODE_VERSION=18
OPENCLAW_ROOT="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
OPENCLAW_PLUGIN_DIR="$OPENCLAW_ROOT/plugins/agentguard"
OPENCLAW_CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-$OPENCLAW_ROOT/openclaw.json}"
QCLAW_ROOT="${QCLAW_STATE_DIR:-$HOME/.qclaw}"
QCLAW_PLUGIN_DIR="$QCLAW_ROOT/plugins/agentguard"
QCLAW_CONFIG_PATH="${QCLAW_CONFIG_PATH:-$QCLAW_ROOT/qclaw.json}"

# ---- Parse arguments ----
TARGET_DIR=""
Expand Down Expand Up @@ -172,6 +175,24 @@ detect_platform() {
return
fi

if [ -d "$HOME/.hermes" ]; then
SKILLS_DIR="$HOME/.hermes/skills/agentguard"
PLATFORM="hermes"
return
fi

if [ -d "$HOME/.qclaw" ]; then
SKILLS_DIR="$HOME/.qclaw/skills/agentguard"
PLATFORM="qclaw"
return
fi

if [ -d "$HOME/.codex" ]; then
SKILLS_DIR="$HOME/.codex/skills/agentguard"
PLATFORM="codex"
return
fi

# Fallback: create Claude Code dir (most common legacy install path).
# Use --target for custom layouts when this default is not desired.
SKILLS_DIR="$HOME/.claude/skills/agentguard"
Expand Down Expand Up @@ -224,7 +245,7 @@ echo "[3/5] Installing scripts..."
mkdir -p "$SKILLS_DIR/scripts"

# Copy script files
for f in checkup-report.js checkup-score.js scan-to-sarif.js guard-hook.js auto-scan.js trust-cli.js action-cli.js; do
for f in checkup-report.js checkup-score.js scan-to-sarif.js guard-hook.js hermes-hook.js auto-scan.js trust-cli.js action-cli.js; do
[ -f "$SKILL_SRC/scripts/$f" ] && cp "$SKILL_SRC/scripts/$f" "$SKILLS_DIR/scripts/" 2>/dev/null || true
done

Expand Down Expand Up @@ -324,6 +345,166 @@ NODE
echo " OK: OpenClaw plugin enabled in $OPENCLAW_CONFIG_PATH"
fi

if [ "$PLATFORM" = "qclaw" ]; then
echo " Enabling QClaw plugin..."
mkdir -p "$QCLAW_PLUGIN_DIR"
AGENTGUARD_DIST_INDEX="$SCRIPT_DIR/dist/index.js" node - "$QCLAW_PLUGIN_DIR/index.js" <<'NODE'
const { writeFileSync } = require('node:fs');
const pluginPath = process.argv[2];
const distIndex = process.env.AGENTGUARD_DIST_INDEX;
writeFileSync(pluginPath, `const { registerOpenClawPlugin } = require(${JSON.stringify(distIndex)});

module.exports = function setup(api) {
registerOpenClawPlugin(api, {
skipAutoScan: false,
});
};
module.exports.default = module.exports;
`);
NODE
cat > "$QCLAW_PLUGIN_DIR/package.json" <<'JSON'
{
"name": "agentguard-qclaw-local",
"private": true,
"type": "commonjs",
"openclaw": {
"extensions": ["./index.js"],
"runtimeExtensions": ["./index.js"]
},
"qclaw": {
"extensions": ["./index.js"],
"runtimeExtensions": ["./index.js"]
}
}
JSON
cat > "$QCLAW_PLUGIN_DIR/openclaw.plugin.json" <<'JSON'
{
"id": "agentguard",
"name": "GoPlus AgentGuard",
"description": "AI agent security framework — blocks dangerous commands, prevents data leaks, and protects secrets"
}
JSON
node - "$QCLAW_CONFIG_PATH" "$QCLAW_PLUGIN_DIR" <<'NODE'
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs');
const { dirname } = require('node:path');
const [configPath, pluginDir] = process.argv.slice(2);
const ensureRecord = (parent, key) => {
const existing = parent[key];
if (existing && typeof existing === 'object' && !Array.isArray(existing)) return existing;
const next = {};
parent[key] = next;
return next;
};
let config = {};
if (existsSync(configPath)) {
const raw = readFileSync(configPath, 'utf8').trim();
config = raw ? JSON.parse(raw) : {};
}
const plugins = ensureRecord(config, 'plugins');
const load = ensureRecord(plugins, 'load');
const entries = ensureRecord(plugins, 'entries');
const agentguard = ensureRecord(entries, 'agentguard');
agentguard.enabled = true;
const paths = Array.isArray(load.paths) ? load.paths.filter((p) => typeof p === 'string') : [];
if (!paths.includes(pluginDir)) paths.push(pluginDir);
load.paths = paths;
if (Array.isArray(plugins.allow)) {
const allow = plugins.allow.filter((id) => typeof id === 'string');
if (!allow.includes('agentguard')) allow.push('agentguard');
plugins.allow = allow;
}
mkdirSync(dirname(configPath), { recursive: true });
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
NODE
echo " OK: QClaw plugin enabled in $QCLAW_CONFIG_PATH"
fi

if [ "$PLATFORM" = "hermes" ]; then
echo " Configuring Hermes hooks..."
HERMES_CONFIG_PATH="$HOME/.hermes/config.yaml" AGENTGUARD_SKILL_DIR="$SKILLS_DIR" node <<'NODE'
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs');
const { dirname } = require('node:path');
const configPath = process.env.HERMES_CONFIG_PATH;
const skillDir = process.env.AGENTGUARD_SKILL_DIR;
const block = ` on_session_start:
- command: "env AGENTGUARD_AUTO_SCAN=1 node \\"${skillDir}/scripts/auto-scan.js\\""
timeout: 30

pre_tool_call:
- matcher: "terminal|execute_code"
command: "node \\"${skillDir}/scripts/hermes-hook.js\\""
timeout: 10
- matcher: "write_file|patch|skill_manage"
command: "node \\"${skillDir}/scripts/hermes-hook.js\\""
timeout: 10
- matcher: "read_file"
command: "node \\"${skillDir}/scripts/hermes-hook.js\\""
timeout: 10
- matcher: "web_search|web_extract|browser_navigate"
command: "node \\"${skillDir}/scripts/hermes-hook.js\\""
timeout: 10

post_tool_call:
- matcher: "terminal|execute_code|write_file|patch|skill_manage|read_file|web_search|web_extract|browser_navigate"
command: "node \\"${skillDir}/scripts/hermes-hook.js\\""
timeout: 5`;
const findNextTopLevel = (lines, start) => {
for (let i = start; i < lines.length; i += 1) {
if (/^\S/.test(lines[i]) && !/^#/.test(lines[i])) return i;
}
return lines.length;
};
const removeManaged = (lines) => {
const events = new Set(['on_session_start', 'pre_tool_call', 'post_tool_call']);
const kept = [];
for (let i = 0; i < lines.length;) {
const match = /^ ([A-Za-z0-9_-]+):\s*(?:#.*)?$/.exec(lines[i]);
if (match && events.has(match[1])) {
i += 1;
while (i < lines.length && !/^ [A-Za-z0-9_-]+:\s*(?:#.*)?$/.test(lines[i]) && !/^\S/.test(lines[i])) i += 1;
continue;
}
kept.push(lines[i]);
i += 1;
}
return kept;
};
const existing = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
const lines = existing.replace(/\s+$/g, '').split(/\r?\n/).filter((line, index, arr) => !(arr.length === 1 && index === 0 && line === ''));
const hooksIndex = lines.findIndex((line) => /^hooks:\s*(?:#.*)?$/.test(line));
let merged;
if (hooksIndex === -1) {
merged = `${lines.length ? `${lines.join('\n')}\n\n` : ''}hooks:\n${block}\n`;
} else {
const hooksEnd = findNextTopLevel(lines, hooksIndex + 1);
merged = [...lines.slice(0, hooksIndex + 1), ...removeManaged(lines.slice(hooksIndex + 1, hooksEnd)), ...block.split('\n'), ...lines.slice(hooksEnd)].join('\n');
}
if (!/^hooks_auto_accept:\s*/m.test(merged)) merged += '\n\nhooks_auto_accept: false';
mkdirSync(dirname(configPath), { recursive: true });
writeFileSync(configPath, `${merged.replace(/\s+$/g, '')}\n`);
NODE
echo " OK: Hermes hooks configured in $HOME/.hermes/config.yaml"
fi

if [ "$PLATFORM" = "codex" ]; then
echo " Writing Codex AgentGuard hook config..."
mkdir -p "$HOME/.codex"
cat > "$HOME/.codex/agentguard-hook.json" <<'JSON'
{
"agentHost": "codex",
"command": "AGENTGUARD_AGENT_HOST=codex agentguard protect",
"actionTypes": {
"shell": "shell",
"fileRead": "file_read",
"fileWrite": "file_write",
"network": "network",
"mcpTool": "mcp_tool"
}
}
JSON
echo " OK: Codex AgentGuard config written to $HOME/.codex/agentguard-hook.json"
fi

# ---- Done ----
echo ""
echo " ✅ GoPlus AgentGuard is installed!"
Expand Down
33 changes: 18 additions & 15 deletions skills/agentguard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,17 @@ cd ~/.openclaw/skills/agentguard && node scripts/checkup-report.js
### Post-Install Guidance

When AgentGuard has just been installed, or the user asks what to do next after
installing AgentGuard, recommend these steps in this order instead of deriving a
longer list from every supported command:
installing AgentGuard, recommend the required setup step below instead of
deriving a longer list from every supported command:

```bash
agentguard init --agent <agent>
agentguard connect
agentguard checkup
agentguard init --agent auto
```

Use the current agent host for `<agent>` when it is known; otherwise leave
`<agent>` as a placeholder.
Use a specific agent such as `hermes`, `qclaw`, `openclaw`, `claude-code`, or
`codex` only when the current host is known and the user wants targeted setup.
Treat `agentguard connect` and `agentguard checkup` as optional follow-ups, not
required post-install steps.

Parse `$ARGUMENTS` to determine the subcommand:

Expand Down Expand Up @@ -120,8 +120,10 @@ If the user writes `/agentguard checkup --against-advisory <id>`, use the CLI co
Help the user configure AgentGuard runtime protection for Hermes Agent.

Hermes does **not** load hooks from `SKILL.md` automatically. Hermes shell hooks
must be present in `~/.hermes/config.yaml`. This skill ships the hook runner at
`scripts/hermes-hook.js` and a copyable template at `hermes-hooks.yaml`.
must be present in `~/.hermes/config.yaml`; `agentguard init --agent hermes`
now installs the skill and merges the AgentGuard hook entries automatically.
This skill ships the hook runner at `scripts/hermes-hook.js` and a copyable
template at `hermes-hooks.yaml`.

### What the Hermes hook protects

Expand Down Expand Up @@ -151,12 +153,13 @@ the shared runtime protection path and syncs pre-tool decisions to Cloud.
```bash
npm install -g @goplus/agentguard
```
3. Read `hermes-hooks.yaml`, replace `AGENTGUARD_SKILL_DIR` with the absolute
skill directory, and show the resulting YAML to the user.
4. Ask for explicit confirmation before editing `~/.hermes/config.yaml`.
5. If confirmed, merge the `hooks:` entries into `~/.hermes/config.yaml`.
Preserve existing hooks and config values. Do not overwrite unrelated user
configuration.
3. Prefer `agentguard init --agent hermes --force` to install and merge the
hook entries automatically.
4. For manual setup, read `hermes-hooks.yaml`, replace
`AGENTGUARD_SKILL_DIR` with the absolute skill directory, and show the
resulting YAML to the user.
5. Ask for explicit confirmation before manually editing
`~/.hermes/config.yaml`.
6. Tell the user to restart Hermes or launch it with one of the first-use
consent options:
```bash
Expand Down
25 changes: 22 additions & 3 deletions src/adapters/openclaw-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,15 @@ function readOpenClawConfigLevel(

type OpenClawBeforeToolCallResult =
| { block: true; blockReason: string }
| { ask: true; askReason: string };
| {
requireApproval: {
title: string;
description: string;
severity?: 'info' | 'warning' | 'critical';
timeoutMs?: number;
timeoutBehavior?: 'allow' | 'deny';
};
};

function runtimeResultToBeforeToolCallResult(
result: ProtectResult | null
Expand Down Expand Up @@ -668,8 +676,13 @@ function runtimeResultToBeforeToolCallResult(

if (decision === 'require_approval' && result.approvalChannel === 'agent') {
return {
ask: true,
askReason: reason,
requireApproval: {
title: 'AgentGuard approval required',
description: reason,
severity: openClawApprovalSeverity(result.decision.riskLevel),
timeoutMs: 60_000,
timeoutBehavior: 'deny',
},
};
}
return {
Expand All @@ -678,6 +691,12 @@ function runtimeResultToBeforeToolCallResult(
};
}

function openClawApprovalSeverity(riskLevel: ProtectResult['decision']['riskLevel']): 'info' | 'warning' | 'critical' {
if (riskLevel === 'critical' || riskLevel === 'high') return 'critical';
if (riskLevel === 'medium') return 'warning';
return 'info';
}

function shouldSurfaceRuntimeApproval(result: ProtectResult): boolean {
return (
result.policySource === 'cloud-decision' ||
Expand Down
Loading
Loading