checkCommand, exported from the cc-safety-net/api subpath. Library API is the signature reference: types, TypeError messages, and what one call reads. This page covers the decisions around it.
In-process check or installed hook
Both paths run the same guard. They differ in who owns the call.
The rule of thumb follows from that last column but is really about ownership. If your users run one of the supported agent CLIs, ship them the install command and let the integration do the work; see Integration architecture for how each agent is wired. If you are the program running the commands, call the function. Building a hook config generator for your own runtime is rebuilding an integration that already exists.
The package root export is the OpenCode plugin object, not the engine. Import from
cc-safety-net/api for checkCommand.Policy resolution in embedded mode
There is no separate embedded configuration. A call reads the same files a hook reads, and thecwd argument decides which project’s files those are.
- User scope.
~/.cc-safety-net/policy.jsonand~/.cc-safety-net/rules/, moved byCC_SAFETY_NET_HOMEwhen it is set. This is the operator’s own baseline, and it applies to every call. - Project scope.
<cwd>/.cc-safety-net/policy.jsonand<cwd>/.cc-safety-net/rules/, resolved from the exact path you pass. There is no walk up through parent directories. A host that tracks a session subdirectory should pass the repository root ascwd, or the committed project policy will not load. - Merge. The project file layers over the user file field by field. Protected-path lists from both scopes are unioned,
auditin a project file is ignored, and every field a project relaxes is reported rather than applied silently. Policy owns the merge contract. - Environment.
CC_SAFETY_NET_LEVELand the capability switches are read fromprocess.envon every call, and the level can only be raised above what the policy files set. See Environment variables.
cwd selects it. A host that manages several checkouts at once gets the right policy for each one by passing the right directory, with no extra API.
Two behaviors are worth knowing before you write the call site. A cwd that does not stat as a readable, searchable directory returns a fail-closed deny rather than throwing, so a mistyped path blocks commands instead of analyzing the wrong project. And a malformed input, such as a missing cwd or a non-string command, throws TypeError, which is a bug in the calling code rather than a verdict about the command.
What the API guarantees
These hold across minor versions, so a host can build on them:- The input shape
{ command, cwd }, withcwdan absolute directory path, and the two result kinds,allowanddenywith areasonstring and an optionalruleId. - A
denymeans do not execute the command, and a throw means the same. A throw is never an allow. cwdanchors policy resolution. Relative command targets and the project’s.cc-safety-net/configuration resolve against thecwdyou pass, neverprocess.cwd().- The API path writes no audit record and makes no network request. A call reads local policy files, filesystem facts, and environment settings; nothing leaves the machine.
reason wording changes with it, so an upgrade can turn an allow into a deny for a command your test fixtures rely on. Branch on kind. Keep reason and ruleId for humans and logs rather than comparing against them, and pin the version in your lockfile the way you pin any other dependency whose behavior you test against.
A worked example: a plugin host
Hosts with a plugin architecture usually expose a pre-execution hook that can veto a tool call. That is the whole integration point. The plugin maps the host’s event to{ command, cwd }, calls checkCommand, and turns a deny into whatever the host’s veto looks like:
- Only command tools are checked.
checkCommandanalyzes shell command text. A host’s read, write, or search tools are not command tools, so route only the shell tool to it and let the others through. cwdis the project root, absolute. Whatever your host calls it, that value selects the project policy and anchors relative paths such as.envin the command.- A throw blocks. The
catchis the fail-closed half of the contract. A host that let a thrown error fall through to execution would turn a broken check into an allow. - A malformed event blocks too. The shipped in-process integrations do exactly this: an event whose command field is missing or not a string is blocked, not skipped, because there is nothing to analyze.
Operational notes
- You own the log. No audit record is written on this path, so a denial that is not logged by the host leaves no trace. The
logscommand and the GUI dashboard show hook and plugin decisions, not embedded ones. - Operators configure you with the normal tools. Because an embedded host reads the same policy files,
status,doctor, andexplainall describe what your host will do, as long as they run in the same directory you pass ascwd. Point users at Explain trace when they ask why a command was blocked. ruleIdis diagnostic. Print it, log it, use it to look up a rule. Do not branch on it.- The call is synchronous. For a batch, loop. There is no async or batched entry point, and adding one would only hide the file reads a call already makes.
- Read the version from the package.
cc-safety-net/package.jsonis an exported subpath, so a host that reports its own dependency versions can include this one.
Related pages
- Library API is the full signature reference for
checkCommand. - Integration architecture describes the installed integrations this function replaces when your host runs its own commands.
- Policy and Environment variables document everything a call reads.
- Blocked commands lists what a deny can be.