Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ForceField

Security hardening for Claude Code. Local detection, pre-action blocking, and a security log you can query.

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-three 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.

Docs: magonia-research.github.io/forcefield-docs

Install

git -c core.hooksPath=/dev/null clone --no-recurse-submodules \
    https://github.com/Magonia-Research/ForceField.git

That is longer than git clone on purpose, and it is the same command ForceField will hand you the next time you clone anything: hooks cannot run out of the repository being fetched, and no config level can recurse submodules behind you. The plain spelling is blocked rather than prompted, so the redirect is the path rather than a suggestion. See the clone redirect.

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.

Optional, for 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.

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.

Pick a posture

Ships as 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
Any git clone (or gh repo clone) that has not disarmed that surface deny, with the hardened command
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
Known attacker behavior 106 compiled SigmaHQ rules: audit, firewall and backup tampering, in-memory execution, discovery, mining warn, ask under strict

Commands are shell-normalized before matching, so ${IFS}, backslash escapes and intra-word quoting do not evade a pattern.

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 DO NOT CLONE then denies the clone itself rather than prompting on it.

Every other clone is redirected, not judged. A plain git clone is blocked, and the reason carries the command that would not have been — the one in Install, with your URL spliced in, in whichever tool you used. Run that and there is no prompt and no block. It clears the zero-false-positive bar for the same reason rm -rf does: every clone has a hardened spelling of itself that fetches the identical tree, so the block forbids no task. The one clone that cannot be hardened is the one asking for submodules on purpose — --recurse-submodules contradicts the flag that would harden it — and that one still prompts rather than blocking. The block does not lift on a patched git, because disabling hooks and refusing submodule recursion are not patches for either CVE.

Check what a hook decides

Feed any hook event JSON on stdin. Empty stdout means allow.

echo '{"tool_name":"Bash","tool_input":{"command":"git submodule update --init --recursive"},"hook_event_name":"PreToolUse"}' \
  | python3 hooks/security_dispatcher.py

On a patched host that 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: submodule_update (context only)\n\nMatched: git submodule update --init\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."}}

A clone is the one shape where a patched git is not the whole answer, so it blocks on any host — and says what to run instead:

echo '{"tool_name":"Bash","tool_input":{"command":"git clone https://github.com/example/repo.git"},"hook_event_name":"PreToolUse"}' \
  | python3 hooks/security_dispatcher.py | python3 -c 'import json,sys; print(json.load(sys.stdin)["hookSpecificOutput"]["permissionDecisionReason"].rsplit("Run instead:",1)[-1])'
  /forcefield:inspect https://github.com/example/repo.git
  git -c core.hooksPath=/dev/null clone --no-recurse-submodules https://github.com/example/repo.git

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.

# Detections that did not enforce: config downgraded or allowlisted
jq -c 'select(.Attributes."forcefield.config_downgraded" == true
              or .Attributes."forcefield.suppressed" == 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

The file sink rotates in-process at a hard size ceiling. On Linux you can hand it to logrotate as well, as yourself, with no root:

scripts/rotation-config.sh              # print the stanza
scripts/rotation-config.sh --install    # write it to ~/.config/forcefield/

On macOS the same command explains why there is nothing to install, since newsyslog refuses any config unless it is running as root, and states the in-process ceiling that stands instead.

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 23 registrations, which 10 of Claude Code's 31 events they use and why not the other 21, the decision ladder, precedence, fail-open
Configuration Trust levels, presets, per-rung mode maps, allowlists, 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, because a security hook that breaks legitimate work gets uninstalled. Run ForceField alongside a sandbox, not instead of one.
  • Guards are heuristics over text. They match commands, not intent. 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. A project-level config file, which a cloned repo can ship, caps at ask.
  • Under bypassPermissions you get deny-only enforcement. A hook ask is discarded rather than shown. A hook deny is absolute in every mode.

Scope limits gives each of these in full, with the reasoning.

To report a vulnerability in ForceField itself, open a private report through GitHub Security Advisories rather than a public issue.

Contributing

Register a new guard in hooks/hooks.json, follow the hook contract (fail-open, stdlib-only, allowlist and logging integration), add assertions to tests/test_plugin.py, and update the tables in docs/hooks.md. Run every suite before opening a PR:

for t in tests/test_*.py; do python3 "$t" || break; done

test_docs.py checks the docs against the tree: cross-links and heading anchors resolve, the file map and suite table match what ships, every counted claim is read out of the code, and every documented log record carries the envelope the code actually emits. If you edit docs/, run scripts/sync-docs.sh to push the change to the published site, or --check to see the drift.

License

GPL-3.0-or-later. See LICENSE.

About

ForceField: deterministic security hardening for Claude Code. Defense-in-depth hooks check every tool call, file access, MCP request, and agent spawn — enforcing container-first execution, SigmaHQ rule matching, and credential/exfiltration/supply-chain/prompt-injection defense in code, not model discretion.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages