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
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,30 @@

All notable changes to HackMyAgent are documented in this file.

## [Unreleased]

### Added
- **AI Visibility Protection plugin** — new 4th plugin in fix-all pipeline that blocks .env from AI tool visibility and encrypts MCP server keys (requires secretless-ai at runtime, optional)
- **Next steps section** after `secure` scan output — recommends `fix-all --with-aim` and shows auto-fixable count
- **Cross-tool recommendations** — suggests `npx secretless-ai init` when credential findings are detected
- **AI visibility scanner checks** — SLAI-001 (credentials in AI context files), SLAI-003 (.env not blocked from AI tools)
- `--fix --dry-run` now shows `[DRY RUN] Would fix:` previews for each auto-fixable finding with summary

### Changed
- Plugin display names: CredVault -> Credential Protection, SignCrypt -> File Signing, SkillGuard -> Skill Safety Scanner
- fix-all pipeline is now 4 plugins: Credential Protection -> AI Visibility Protection -> File Signing -> Skill Safety Scanner
- Scanner fix messages for SKILL-001, HEARTBEAT-002/003, AIM-001/002, DNA-002 now point to `fix-all --with-aim`
- Project type detection: SKILL.md alone no longer triggers "OpenClaw Agent" label (renamed to "AI Agent")
- Duplicate findings at the same file:line are deduplicated (highest severity kept, shows "+ N related")
- Registry contribution message is transparent: shows `(--no-contribute to opt out)`
- Contribution prompt only appears after 3 scans in interactive TTY mode

## [0.10.2] - 2026-03-16

### Fixed
- Trust score now displays as `47/100` instead of raw decimal `0.47` for consistency with opena2a CLI

## [Unreleased]
## [0.10.1]

### Added
- UNICODE-STEGO-001: Invisible Unicode codepoint detection (variation selectors U+FE00-FE0F, tag characters U+E0100-E01EF)
Expand Down
2 changes: 1 addition & 1 deletion __tests__/plugins/credvault/deep.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ describe('CredVaultPlugin (deep)', () => {

it('status returns correct metadata', async () => {
const status = await plugin.status();
expect(status.name).toBe('CredVault');
expect(status.name).toBe('Credential Protection');
expect(status.version).toBe('0.1.0');
});
});
Expand Down
2 changes: 1 addition & 1 deletion __tests__/plugins/credvault/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe('CredVaultPlugin', () => {
describe('status', () => {
it('returns plugin status', async () => {
const status = await plugin.status();
expect(status.name).toBe('CredVault');
expect(status.name).toBe('Credential Protection');
expect(status.version).toBe('0.1.0');
});
});
Expand Down
78 changes: 78 additions & 0 deletions __tests__/plugins/secretless/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { SecretlessPlugin } from '../../../src/plugins/secretless';

describe('SecretlessPlugin', () => {
let tmpDir: string;
let plugin: SecretlessPlugin;

beforeEach(async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'secretless-test-'));
plugin = new SecretlessPlugin();
await plugin.init();
});

afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

describe('scan', () => {
it('reports SLAI-000 when secretless-ai is not installed', async () => {
// secretless-ai is not a devDependency of hackmyagent,
// so the dynamic import will fail and the plugin should degrade gracefully
const findings = await plugin.scan(tmpDir);

// Should either return SLAI-000 (not installed) or real findings if
// secretless-ai happens to be globally installed
if (findings.length === 1 && findings[0].id === 'SLAI-000') {
expect(findings[0].id).toBe('SLAI-000');
expect(findings[0].title).toBe('Secretless AI not installed');
expect(findings[0].severity).toBe('medium');
expect(findings[0].autoFixable).toBe(false);
} else {
// secretless-ai is available globally — findings are real scan results
expect(Array.isArray(findings)).toBe(true);
}
});
});

describe('fix', () => {
it('returns empty remediations when secretless-ai is not installed', async () => {
const remediations = await plugin.fix(tmpDir);
// Without secretless-ai, fix should be a no-op
expect(remediations).toEqual([]);
});

it('returns dry-run previews for auto-fixable findings', async () => {
const remediations = await plugin.fix(tmpDir, { dryRun: true });
// Either empty (no secretless-ai) or dry-run entries
expect(Array.isArray(remediations)).toBe(true);
for (const r of remediations) {
expect(r.description).toMatch(/^Would fix:/);
}
});
});

describe('status', () => {
it('returns plugin status', async () => {
const status = await plugin.status();
expect(status.name).toBe('AI Visibility Protection');
expect(status.version).toBe('0.1.0');
// active depends on whether secretless-ai is installed
expect(typeof status.active).toBe('boolean');
});
});

describe('metadata', () => {
it('has correct metadata', () => {
expect(plugin.metadata.packageName).toBe('secretless-ai');
expect(plugin.metadata.displayName).toBe('AI Visibility Protection');
expect(plugin.metadata.findings).toContain('SLAI-001');
expect(plugin.metadata.findings).toContain('SLAI-002');
expect(plugin.metadata.findings).toContain('SLAI-003');
expect(plugin.metadata.scoreImprovement).toBe(20);
});
});
});
23 changes: 15 additions & 8 deletions docs/PLUGIN_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,23 @@ HackMyAgent ships with 3 built-in plugins:

| Plugin | Factory | What it does |
|--------|---------|-------------|
| CredVault | `createCredVaultPlugin()` | Credential detection and migration |
| Signcrypt | `createSigncryptPlugin()` | Config file signing and integrity |
| Skillguard | `createSkillguardPlugin()` | Skill/plugin safety scanning |
| Credential Protection | `createCredVaultPlugin()` | Scan for hardcoded secrets, replace with env var references |
| AI Visibility Protection | `createSecretlessPlugin()` | Block .env from AI tools, encrypt MCP server keys |
| File Signing | `createSigncryptPlugin()` | Sign skill and heartbeat files with Ed25519 |
| Skill Safety Scanner | `createSkillguardPlugin()` | Detect dangerous patterns (RCE, exfiltration, reverse shells) and pin hashes |

```typescript
import { createCredVaultPlugin, createSigncryptPlugin, createSkillguardPlugin } from 'hackmyagent';

const credVault = createCredVaultPlugin();
const signcrypt = createSigncryptPlugin();
const skillguard = createSkillguardPlugin();
import {
createCredVaultPlugin,
createSecretlessPlugin,
createSigncryptPlugin,
createSkillguardPlugin,
} from 'hackmyagent';

const credProtection = createCredVaultPlugin();
const aiVisibility = createSecretlessPlugin(); // requires secretless-ai at runtime
const fileSigning = createSigncryptPlugin();
const skillSafety = createSkillguardPlugin();
```

## AIM Integration
Expand Down
34 changes: 23 additions & 11 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3879,6 +3879,7 @@ function generateAttackHtmlReport(report: AttackReport): string {
// --- fix-all: Run all OpenClaw plugins to scan and remediate ---

import { createPlugin as createCredVaultPlugin } from './plugins/credvault';
import { createPlugin as createSecretlessPlugin } from './plugins/secretless';
import { createPlugin as createSigncryptPlugin } from './plugins/signcrypt';
import { createPlugin as createSkillguardPlugin } from './plugins/skillguard';
import { AIMCore } from '@opena2a/aim-core';
Expand All @@ -3902,27 +3903,35 @@ program
.description(`Run all OpenA2A security plugins to scan and auto-fix agent issues

Runs the full plugin suite in order:
1. SkillGuard — hash pinning, tamper detection, dangerous patterns
2. SignCrypt — Ed25519 signing, heartbeat hash pins
3. CredVault — credential detection, env var replacement
1. Credential Protection — find hardcoded secrets, replace with env vars
2. AI Visibility Protection — block .env from AI tools, encrypt MCP keys
3. File Signing — sign skills and heartbeats with Ed25519
4. Skill Safety Scanner — detect dangerous patterns, pin hashes

Each plugin scans for findings, then auto-fixes what it can.
Dangerous patterns (reverse shells, exfil, etc.) require manual review.

Step 2 requires secretless-ai (npm install -g secretless-ai). If not
installed, the plugin reports this and continues with the remaining steps.

Use --with-aim to create a cryptographic identity for your agent.
This enables automatic file signing, audit logging, and trust scoring
so you don't need to manage keys or track files manually.

Exit code 1 if critical/high issues remain after fixing.

Examples:
$ hackmyagent fix-all Scan and fix current directory
$ hackmyagent fix-all ./my-agent Scan specific directory
$ hackmyagent fix-all --with-aim Create identity + sign + audit (recommended)
$ hackmyagent fix-all --dry-run Preview fixes without applying
$ hackmyagent fix-all --scan-only Scan without fixing
$ hackmyagent fix-all --json JSON output for CI
$ hackmyagent fix-all --with-aim Enable AIM identity and audit`)
$ hackmyagent fix-all --json JSON output for CI`)
.argument('[directory]', 'Agent directory to scan (default: current directory)', '')
.option('--dry-run', 'Preview fixes without applying them')
.option('--scan-only', 'Only scan, do not fix')
.option('--json', 'Output as JSON (for scripting/CI)')
.option('--with-aim', 'Initialize AIM Core for identity-aware audit logging')
.option('--with-aim', 'Create agent identity for automatic signing, audit logging, and trust scoring')
.option('-v, --verbose', 'Show all findings including passed plugins')
.action(
async (
Expand Down Expand Up @@ -3984,12 +3993,15 @@ Examples:
}

// Create and initialize plugins in execution order
// Order matters: CredVault replaces creds, SignCrypt signs skills,
// SkillGuard pins last so hashes reflect the final file state.
// 1. CredVault finds hardcoded secrets, replaces with ${VAR}
// 2. Secretless blocks .env from AI visibility (completes the credential lifecycle)
// 3. SignCrypt signs skill and heartbeat files
// 4. SkillGuard pins hashes last so they reflect the final file state
const pluginFactories: Array<{ name: string; create: () => OpenA2APlugin }> = [
{ name: 'CredVault', create: createCredVaultPlugin },
{ name: 'SignCrypt', create: createSigncryptPlugin },
{ name: 'SkillGuard', create: createSkillguardPlugin },
{ name: 'Credential Protection', create: createCredVaultPlugin },
{ name: 'AI Visibility Protection', create: createSecretlessPlugin },
{ name: 'File Signing', create: createSigncryptPlugin },
{ name: 'Skill Safety Scanner', create: createSkillguardPlugin },
];

const plugins: Array<{ name: string; plugin: OpenA2APlugin }> = [];
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/credvault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ function createEnvExample(agentDir: string, findings: Finding[]): string | null

export const metadata: PluginMetadata = {
packageName: 'hackmyagent',
displayName: 'CredVault',
description: 'Credential protection — scan for hardcoded secrets, move to encrypted store, per-skill isolation',
displayName: 'Credential Protection',
description: 'Scan for hardcoded secrets and replace with environment variable references',
version: VERSION,
findings: ['CRED-001', 'CRED-002', 'CRED-003', 'CRED-004'],
scoreImprovement: 25,
Expand Down
Loading
Loading