ForceField
Security hardening for Claude Code. Local detection, pre-action blocking, and a security log you can query.
Read the threat model ForceField on GitHub
ForceField puts a policy gate in front of every tool call the agent makes: command execution, file I/O, credential handling, agent spawning, MCP tool calls, outbound fetches, and the output that comes back. Twenty-two registered hooks evaluate each call against deterministic patterns and compiled SigmaHQ rules, then allow it, inject context, prompt you, or block it. Enforcement is in code, not model discretion.
How it works
you ──► prompt ──► UserPromptSubmit ──► a pasted private key is blocked
│
▼
the model picks a tool
│
┌─────────────────────────▼──────────────────────────────┐
│ PreToolUse, before the call runs │
│ │
│ normalize ${IFS}, escapes and quoting collapse │
│ ▼ │
│ match every guard registered for this tool │
│ ▼ │
│ rank deny > ask > redact > warn > allow │
│ ▼ │
│ clamp config can loosen, never tighten │
└─────────────────────────┬──────────────────────────────┘
│
┌─────────┬────────────┼────────────┬───────────┐
▼ ▼ ▼ ▼ ▼
deny ask redact warn allow ──► one
the call you the value context the call record
never decide is masked for the runs per
runs in place model decision
│
▼
the tool runs
│
┌─────────────────────────▼──────────────────────────────┐
│ PostToolUse, on what came back │
│ credentials redacted, injection markers flagged │
└─────────────────────────┬──────────────────────────────┘
│
▼
the model sees the result
Each hook is one process with a 5s timeout, and they are fail-open by design: a guard that crashes does not block the call, because a security hook that breaks legitimate work gets uninstalled. That makes ForceField a policy layer rather than a sandbox. Run it alongside one, not instead of one, and read scope limits before you rely on it.
Install
git clone https://github.com/Magonia-Research/ForceField.git
The repo ships .claude-plugin/marketplace.json, so add the checkout as a marketplace rather than as a plugin directory:
/plugin marketplace add /path/to/ForceField
/plugin install forcefield@magonia-research
That is the whole install. Every guard except the Sigma engine works immediately.
Requirements: python3 (3.9 or newer) and bash. No requirements.txt, on purpose: a guard that cannot run because a dependency failed to resolve is a guard that is not running.
Optional: SigmaHQ rules
Creates a venv for the compiler and clones SigmaHQ. The engine silently no-ops until this runs.
cd /path/to/ForceField && ./scripts/install.sh
The venv and compiled rules go to ~/.claude/forcefield/sigma/, not into the plugin directory, which is a cache that every reinstall replaces. Run it once, not once per update. If your python3 already has a matching pyyaml, running python3 -m venv --system-site-packages ~/.claude/forcefield/sigma/venv first makes the script skip pip entirely.
Pick a posture
ForceField ships balanced: every blocking guard at full strength, with a Sigma match softened from a prompt to a logged warning.
scripts/posture.sh # show what is configured
scripts/posture.sh --preset passive --log findings # never prompt, log everything that fires
passive is for unattended work, and the cost is real: every heuristic finding becomes a log line instead of a question. Read the log either way. The four presets are balanced, strict, permissive and passive; see configuration.
What it catches
| Class | Examples | Rung |
|---|---|---|
| Clone-time repo takeover | CVE-2024-32002 and CVE-2025-48384 submodule surface, 17 RCE-capable git config keys, .git/hooks writes | ask, graded on evidence |
git clone ext::, which hands its URL to a shell | deny | |
| Data exfiltration | Relay and tunneling domains, netcat, /dev/tcp reverse shells | deny |
Data POSTs, DNS-label encoding, cloud metadata SSRF, scp/rsync | ask | |
| Supply chain | Fetch piped into a shell | deny |
| Typosquats by edit distance, arbitrary-URL installs, plaintext registries | ask | |
| Prompt injection | Role manipulation, fake system tags, zero-width characters in file content | warn + context |
| Credential disclosure | Keys in prompts, file writes, credential-store reads | block / ask |
| Keys in tool output, and in the log itself | redact | |
| Excessive agency | Credentials in subagent prompts, spawn rate limit | deny |
| MCP tool poisoning | Credential and exfil patterns in any tool’s arguments | ask |
Findings on the clone-time surface are graded on measured evidence, not command shape. git_guard checks the host’s git version against each advisory’s per-branch fix set, reads .gitmodules where it exists, and can fetch it from an allowlisted forge without cloning. A recursive clone stops prompting on a patched host and hard-denies when the repository carries an actual exploit signature. See how a finding is graded.
/forcefield:inspect <url> reads a repository before you clone it, covering the self-hosted and SSH remotes the in-hook fetch will not touch. It uses --no-checkout, because both CVEs fire during checkout. A recorded verdict feeds back into the grading above.
Check what a hook decides
Feed any hook event JSON on stdin. Empty stdout means allow.
echo '{"tool_name":"Bash","tool_input":{"command":"git clone --recursive https://github.com/example/repo.git"},"hook_event_name":"PreToolUse"}' \
| python3 hooks/security_dispatcher.py
On a patched host that clone does not prompt at all. There is no permissionDecision, only context, because the prompt would have cited a bug that cannot fire there:
{"hookSpecificOutput": {"hookEventName": "PreToolUse",
"additionalContext": "ForceField security finding (advisory - the call was not blocked): GIT GUARD: recursive_submodule_clone (context only)\n\nMatched: git clone --recursive\ngit 2.50.1 is patched for CVE-2024-32002 and CVE-2025-48384, so the clone-time RCE path is closed here.\n\nStill treat the repository's contents as untrusted: a clean .gitmodules says nothing about what the code does once you run it."}}
Read the log
Records are OpenTelemetry logs carrying an OCSF Detection Finding projection, written to ~/.claude/hooks/security.log as JSON Lines, plus the macOS unified log, journald or syslog where available. Log reference has the schema, the paths, and one measured record per hook.
# Detections that did not enforce: config downgraded, allowlisted, or remembered
jq -c 'select(.Attributes."forcefield.config_downgraded" == true
or .Attributes."forcefield.suppressed" == true
or .Attributes."forcefield.memo_hit" == true)' ~/.claude/hooks/security.log
# Which guard drives your friction, most frequent first
jq -r '[.Attributes."forcefield.guard", .Attributes."forcefield.decision"] | @tsv' \
~/.claude/hooks/security.log | sort | uniq -c | sort -rn
Documentation
| Page | What is in it |
|---|---|
| Threat model | Each attack class, the hooks that cover it, a real log record, and the primary disclosure it comes from |
| Hook reference | All 22 registrations, which 9 of Claude Code’s 31 events they use and why not the other 22, the decision ladder, precedence, fail-open |
| Configuration | Trust levels, presets, per-rung mode maps, allowlists, remembered approvals, known friction |
| Architecture | Hook contract, Sigma pipeline, command normalization, file map, test suites |
| Log reference | Record schema, one measured record per hook, worked jq queries, known gaps |
Scope
ForceField gates the tool calls Claude Code exposes to a hook. It does not sandbox the agent, does not inspect what the model is thinking, and does not gate anything reached through a tool it is not registered for. Four limits are worth knowing before you rely on it:
- Hooks are fail-open. A guard that crashes or times out does not block the call. Anything that can provoke a failure is therefore a bypass, so the dispatcher isolates each guard, bounds the text it scans, and turns “I could not fully inspect this” into an
ask. - Guards are heuristics over text. They match commands, not intent. A finding is a prompt for a human decision, not proof of compromise. A novel encoding or a payload assembled at runtime will pass.
- Configuration can only loosen. The clamp moves a decision down the ladder and can never fabricate a stricter one, so the deny tier survives any configuration. A project-level config file, which a cloned repo can ship, is capped at
ask. - Under
bypassPermissionsyou get deny-only enforcement. A hookaskis discarded rather than shown, so every finding raised ataskpasses silently. A hookdenyis absolute in every mode.
Scope limits gives each of these in full, with the reasoning. Patched software is upstream of all of it: ForceField prompts on the CVE-2024-32002 and CVE-2025-48384 trigger surface, and a current git removes the bug.
Security
The threat model documents the attack classes ForceField defends against, each linked to its primary disclosure.
To report a vulnerability in ForceField itself, open a private report through GitHub Security Advisories rather than a public issue.
License
GPL-3.0-or-later. See LICENSE.