Securing Claude Code with Synapse

Synapse ships with a demo route extension that can be used as a Claude Code hook handler to control Claude Code through Cerbos policies. Administrators can use MDM or server-managed settings to enforce hooks over the whole organization and ensure that security requirements are being met.

To enable the route extension, add the following configuration block to the Synapse configuration file.

extensions:
  routeExtensions:
    claude:
      extension:
        extensionURL: "system://claude"
      routes:
        "/claude": ["POST"]

Now configure Claude Code hooks to invoke the above endpoint. There are multiple ways to do this dependening on your preference. For production deployments, consider using MDM or server-managed settings as it prevents users from overriding the configuration. The following snippet demonstrates a typical hook configuration.

{
    "hooks": {
        "PermissionRequest": [
            {
                "matcher": "*",
                "hooks": [
                    {
                        "type": "http",
                        "statusMessage": "Checking with Synapse",
                        "url": "https://Synapse.example.com:3594/ext/claude",
                        "headers": {
                            "X-Claude-User-Roles": "claude"
                        }
                    }
                ]
            }
        ],
        "PreToolUse": [
            {
                "matcher": "*",
                "hooks": [
                    {
                        "type": "http",
                        "statusMessage": "Checking with Synapse",
                        "url": "https://Synapse.example.com:3594/ext/claude",
                        "headers": {
                            "X-Claude-User-Roles": "claude"
                        }
                    }
                ]
            }
        ],
        "PostToolUse": [
            {
                "matcher": "*",
                "hooks": [
                    {
                        "type": "http",
                        "statusMessage": "Checking with Synapse",
                        "url": "https://Synapse.example.com:3594/ext/claude",
                        "headers": {
                            "X-Claude-User-Roles": "claude"
                        }
                    }
                ]
            }
        ],
        "PostToolUseFailure": [
            {
                "matcher": "*",
                "hooks": [
                    {
                        "type": "http",
                        "statusMessage": "Checking with Synapse",
                        "url": "https://Synapse.example.com:3594/ext/claude",
                        "headers": {
                            "X-Claude-User-Roles": "claude"
                        }
                    }
                ]
            }
        ]
    }
}

With this configuration, every tool call made by Claude Code will be checked by Synapse. Since there are no policies defined yet, everything will be allowed by default. If audit log capture is enabled, all the requests will be logged and available for analysis. This is a good way to do a pass-through or a dry-run to gather data about usage before writing policies.

Writing Cerbos policies

Currently, Claude Code only supports HTTP hooks for a subset of events. If you need to secure other events, use a command hook that simply uses curl to call the Synapse endpoint.

When the hook endpoint is called, Synapse converts the input to a Cerbos CheckResources request of the following form.

  • principal.id ⇒ If the request includes a X-Claude-User header, the value from that header. Otherwise, claude_user.

  • principal.roles ⇒ If the request includes a X-Claude-User-Roles header, the value(s) from that header. Otherwise, claude_user.

  • resource.kind ⇒ Hook event name. E.g. PreToolUse.

  • resource.attr ⇒ Hook payload. See hook event documentation for more details.

  • resource.scope ⇒ Optional. Set only if the X-Cerbos-Scope header is present in the request.

  • action ⇒ For tool call hooks (PermissionRequest, PreToolUse, PostToolUse, PostToolUseFailure) the name of the tool (e.g. Bash). For other hooks, it’s fixed to event.

If a policy is found and returns a decision of either EFFECT_ALLOW or EFFECT_DENY, it’s converted to the appropriate allow or deny response for the hook. It’s possible to provide extra information to attach to the response (e.g. deny reason, extra context etc.) using policy outputs. Note that the output from the policy is merged verbatim into the hook response and it must be a valid response as expected by that particular hook. Invalid or incorrect output values might cause Claude Code to ignore the response from the Hook and continue the action even if it has been denied.

The following example demonstrates a PreToolUse policy that prevents reads or writes from sensitive locations and bans Bash altogether.

---
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: PreToolUse
  version: default
  rules:
    - actions: ["Read", "Write"]
      effect: EFFECT_DENY
      roles: ["*"]
      condition:
        match:
          any:
            of:
              - expr: |-
                  R.attr.tool_input.file_path.startsWith("/etc")
              - expr: |-
                  R.attr.tool_input.file_path.startsWith("/top/secret")
      output:
        when:
          ruleActivated: |-
            {"hookSpecificOutput": {"permissionDecision": "deny", "permissionDecisionReason": "Accessing top secret data is forbidden"}}

    - actions: ["Bash"]
      effect: EFFECT_DENY
      roles: ["*"]
      output:
        when:
          ruleActivated: |-
            {"hookSpecificOutput": {"permissionDecision": "deny", "permissionDecisionReason": "No Bash for you"}}

    - actions: ["*"]
      effect: EFFECT_ALLOW
      roles: ["*"]
      output:
        when:
          ruleActivated: |-
            {"hookSpecificOutput": {"permissionDecision": "allow", "permissionDecisionReason": "Go ahead"}}

With the above policy in place, attempting to do an action that invokes Bash would fail.

● Bash(mktemp -d /foo/tmp.XXXXXX)
  ⎿  PreToolUse:Bash hook returned blocking error
  ⎿  No Bash for you
  ⎿  Error: No Bash for you

● The Bash tool is being blocked by a pre-tool hook (No Bash for you). I don't have another way to create a directory without it. You may want to check your hooks configuration or create it manually with:

  mkdir /foo/tmp_<timestamp>