Skip to content

Open source · Apache-2.0

Keep every client in bounds.

Zence is a task-scoped policy firewall for Claude Code. It resolves the assets a tool call touches against DataHub, and refuses the ones that belong to a different client — before the call runs.

The problem

Every individual step is valid. The mistake is the combination.

Freelancers, agencies and consultancies run Claude Code across several clients from one machine. Claude Code has no concept of which client is in scope right now.

SELECT l.email, p.phone
FROM   northstar.marketing_leads  l   -- Client A  (you are here)
JOIN   bluepeak.patient_contacts  p   -- Client B  (you are not)
  ON   l.email = p.email
Valid SQL. Both tables exist. The developer has credentials for both. Nothing here is an error — until you know which client the repository belongs to.

A linter sees well-formed SQL. The warehouse sees an authorised user. The agent sees a reasonable way to answer the question it was asked.

The only place this is visibly wrong is in the metadata: those two tables sit in different domains, and one of them carries personal data at column level.

That is a question a catalog can answer. Zence asks it, on every tool call, before the call runs.

Real output

Three decisions, unrolled

Every field below is real output from zence evaluate --json, copied into this page by a build step. Nothing is a mock-up.

Synthetic scenario. Northstar Commerce and BluePeak Health are fictional. These are real Zence decisions, recorded from actual runs against the demo catalog — not mock-ups — but no real company or person is represented.

Every part of this is valid. The SQL parses, both tables exist, the developer has credentials for both. The mistake only exists at the level of boundary — and nothing else in the loop is tracking that.

DeniedZR-001

Cross-client PII access

  1. What the developer asked for

    Blend our Northstar leads with the BluePeak patient contact export so we can see overlap.

  2. What Claude tried — Write

    SELECT l.email, p.phone
    FROM   northstar.marketing_leads  l
    JOIN   bluepeak.patient_contacts p
      ON   p.email = l.email
  3. What Zence extracted

    • northstar.marketing_leadshigh confidence · sql
    • bluepeak.patient_contactshigh confidence · sql

    intent: codegen, read

  4. What DataHub said

    • urn:li:dataset:(urn:li:dataPlatform:snowflake,bluepeak.patient_contacts,PROD)

    Classified urn:li:tag:PII · columns email, phone tagged at field level

  5. Decision — ZR-001

    bluepeak.patient_contacts belongs to BluePeak Health, but this session is bounded to Northstar Commerce. It carries urn:li:tag:PII, and columns email, phone are classified at field level. Reading it here would move another client's personal data into this workspace.

    policy v1.0.0 · enforce mode · risk critical · via rule

  6. What Claude is told to do instead

    Use an asset inside Northstar Commerce. If you genuinely need to work on BluePeak Health, switch to that client's workspace so the correct boundary applies.

    The remediation is the point. A bare refusal invites a retry with a variation; naming the alternative turns it into a redirect.

Rendered from cross-client-pii.json — real output of zence evaluate --json

Three more scenarios →

How it answers

Three outcomes, not two

A tool that can only allow or block forces a choice between being useful and being safe. Most real situations are the middle one.

Allowed

In domain, permitted environment, nothing sensitive. The hook returns an empty response and the developer sees nothing at all.

Approval required

Production, deprecated, unowned, or something a critical dashboard depends on. You decide; both outcomes are recorded.

Denied

Cross-client PII, cross-client writes, destructive production operations. Blocked before execution, with the evidence.

When Zence cannot reach DataHub, it does not fall back to allowing. An operation it cannot verify becomes an ask, and it says that the catalog was unreachable rather than implying the asset was clean.

How it works

A hook, a catalog, and a deterministic engine

Claude Code runs Zence before each tool call. Six steps, none of which involve asking a model whether something is allowed.

  1. 01

    Normalize

    The tool call becomes an action with an intent — read, write, mutate, destructive.

  2. 02

    Extract

    SQL through a real parser, plus dbt, shell, YAML recipes and MCP arguments. CTEs and aliases are excluded; columns are attributed through aliases.

  3. 03

    Resolve

    Each reference is looked up in DataHub: domain, owners, tags, glossary terms, lifecycle, schema, and two hops of downstream lineage.

  4. 04

    Evaluate

    Rules are field/predicate pairs over that evidence. No expression language, no eval, no model in the decision path.

  5. 05

    Decide

    Exactly one of allow, ask, deny — with the rule that fired, the evidence, and a remediation.

  6. 06

    Record

    The decision is logged, and written back to DataHub as a durable document at session end.

DataHub

The catalog is the source of truth, on both the read and the write path

Zence has no opinions of its own about your data. Everything it enforces is something DataHub already knows.

Read

Domain and ownership, dataset and column-level tags, glossary terms, lifecycle and deprecation status, schema fields, and two hops of downstream lineage — which is what makes a lineage-aware approval a real decision rather than a configured constant.

Write

At session end Zence upserts a decision document linked to the assets involved, plus a zence.last_review structured property. The document id is deterministic, so finalizing twice updates the record instead of duplicating it.

The DataHub MCP Serveris the surface Zence intercepts — it is how Claude reads the catalog, so it is where a cross-client lookup shows up first. Zence’s own evidence lookups go through the DataHub Python SDK, because a hook must be deterministic and cannot borrow the agent’s MCP connection.

Install

Two commands, then a policy file

Zence needs uv, Claude Code, and a DataHub instance. Nothing else, and no hosted service.

/plugin marketplace add AmirmLotfy/zence
/plugin install zence@zence
Add the marketplace and install the plugin.
zence init --client "Northstar Commerce" \
           --domain "urn:li:domain:northstar-commerce"
Then, in each client repository, describe its boundary. Starts in audit mode — nothing is blocked until you switch to enforce.

What it is not

The honest limits

Zence reduces accidental and agent-mediated mistakes inside the Claude Code workflow. That is a real thing to be, and it is not the same as being a sandbox.

Out of scope

Anything outside Claude Code. A user running the same query in a shell, a notebook, or a BI tool is not intercepted, and Zence does not pretend otherwise.

Not a substitute

Correct grants in your warehouse remain the first line of defence. Zence is the second one — the one that catches the mistake nobody meant to make.

The full threat model →