Security Best Practices for FabrCore Plugins

Eric Brasher February 21, 2026 at 11:21 AM 5 min read

When building plugins that execute system commands or interact with the filesystem, security is your responsibility. FabrCore gives you the tools to build powerful agent capabilities — but with that power comes the obligation to implement proper safeguards. Here are the best practices.

FabrCore's Security Boundary

FabrCore doesn't include command execution tools. There's no built-in "run this shell command" plugin, and that's by design. Command execution is a plugin/application concern, not a framework concern.

This means security validation belongs with the execution code, not in the framework. FabrCore provides the agent infrastructure — message routing, grain lifecycle, plugin discovery, health monitoring. What those plugins do is your domain, and securing what they do is your responsibility.

This is the right boundary. A framework that tries to be a security sandbox for arbitrary plugins ends up being neither a good framework nor a good sandbox. Instead, FabrCore stays out of the way and lets you implement security at the level where you have the most context: the plugin itself.

Allowlists Over Blocklists

If your plugin executes commands, the single most important security decision is: use an allowlist, not a blocklist.

Blocklists are trivially bypassable. Suppose you block rm. An attacker can use /bin/rm, $(echo rm), variable expansion, hex encoding, or any of dozens of other shell tricks. Every blocklist is a game of whack-a-mole where the attacker has infinite creativity and you have a finite list.

Allowlists flip the equation. Instead of trying to enumerate everything that's dangerous (impossible), you enumerate what's permitted (finite and knowable). Everything else is rejected by default.

Allowlist Implementation
private static readonly HashSet<string> AllowedCommands = new(StringComparer.OrdinalIgnoreCase)
{
    "git status", "git log", "git diff",
    "dotnet build", "dotnet test",
    "npm install", "npm run build"
};

public static bool IsAllowed(string command)
    => AllowedCommands.Any(allowed =>
        command.StartsWith(allowed, StringComparison.OrdinalIgnoreCase));

This is simple, auditable, and secure. When someone reviews the code, they can see exactly what commands are permitted. There's no ambiguity, no edge cases to reason about. If a command isn't in the list, it doesn't run.

OS-Level Sandboxing

String matching — even with an allowlist — is only one layer of defense. For plugins that execute system commands, OS-level sandboxing provides a much stronger security boundary:

  • Containers: Run agent processes in containers with minimal base images. If a command escapes your allowlist, the container limits the blast radius. Mount only the directories the agent needs, with read-only access where possible.
  • Restricted Users: Run the agent process as a non-root user with minimal permissions. Even if arbitrary command execution is achieved, the user account limits what damage can be done.
  • Seccomp Profiles: On Linux, seccomp profiles restrict which system calls a process can make. Block calls like execve (for spawning new processes) at the kernel level if your agent shouldn't be executing commands at all.
  • Network Policies: Restrict outbound network access at the container or firewall level. An agent that only needs to call an LLM API shouldn't have access to your internal network.

These mechanisms are more effective than string matching because they operate at a lower level — the OS kernel, not your application code. A creative attacker might find a way around your allowlist, but they can't bypass a seccomp profile that blocks execve.

Validate at Boundaries

Validate user input at the API boundary — the point where external data enters your system. This is where untrusted input first arrives, and it's the natural place to enforce security constraints.

Trust internal code paths. Once input has been validated at the boundary, the code that processes it doesn't need to re-validate at every method call. Scattering validation checks across every internal method creates noise, makes the code harder to read, and gives a false sense of thoroughness.

In FabrCore terms, the boundary is your plugin's tool method — the method the LLM calls. That's where you validate the command, check the allowlist, sanitize arguments, and enforce rate limits. The internal helper methods that actually execute the work can trust that they've received validated input.

Boundary Validation Pattern
[Description("Executes an allowed development command")]
public async Task<string> ExecuteCommand(
    [Description("The command to execute")] string command)
{
    // Validate at the boundary
    if (!IsAllowed(command))
        return $"Command not permitted: {command}";

    // Sanitize arguments
    var sanitized = SanitizeArguments(command);

    // Internal execution trusts validated input
    return await RunProcessAsync(sanitized);
}

Don't Trust a Blocklist

A string-matching blocklist creates a false sense of security. It looks like you're doing something about safety, but in practice it catches only the most naive attempts. Consider what a blocklist for "dangerous commands" would look like:

  • Block rm? What about unlink, shred, find -delete?
  • Block curl? What about wget, nc, python -c "import urllib..."?
  • Block semicolons? What about &&, ||, backticks, $(), pipes?

Every entry you add to a blocklist is an admission that you can't enumerate the threat surface. And you can't — shell languages are Turing-complete. There is no finite blocklist that covers all dangerous inputs.

If you can't enumerate what's allowed, consider whether the plugin should execute arbitrary commands at all. Maybe the right design is a plugin with specific, named operations (BuildProject(), RunTests(), GetGitStatus()) rather than a generic command executor. Named operations have a fixed, auditable surface area.

Learn More

Security is a spectrum, not a checkbox. Start with an allowlist, layer on OS-level sandboxing, validate at boundaries, and question whether generic command execution is the right abstraction for your use case.

Check out the full documentation for more details on building FabrCore plugins and tools.


Eric Brasher

Builder of FabrCore and OpenCaddis.