carrick.json
Per-repo config that tells the scanner which services a repo contains and which env vars and domains name internal services versus third-party APIs.
carrick.json defines service boundaries within a repository and classifies outbound network calls that construct target URLs from environment variables or URL prefixes. This configuration enables the scanner to distinguish internal calls to other services in your organisation from external calls to third-party APIs.
carrick.json configures the static analysis scanner, whereas AGENTS.md and CLAUDE.md provide operational rules for AI coding agents.
Single-service configuration
For a repository containing a single service, define the service name and classification lists at the root of carrick.json:
{
"serviceName": "order-service",
"internalEnvVars": ["USER_SERVICE_URL", "INVENTORY_API"],
"externalEnvVars": ["STRIPE_API", "GITHUB_API"],
"internalDomains": ["https://api.yourcompany.com"],
"externalDomains": ["https://api.stripe.com", "https://api.github.com"]
}
Field reference
| Field | Type | Description |
|---|---|---|
serviceName |
string | Identifier for this service displayed in MCP responses and the dashboard. Defaults to the repository name if omitted. |
internalEnvVars |
string array | Environment variables whose values resolve to internal services in your organisation. Calls constructed using these variables are validated against the index. |
externalEnvVars |
string array | Environment variables whose values resolve to third-party APIs. Calls constructed using these variables are ignored during contract verification. |
internalDomains |
string array | URL prefixes for internal services. Outbound calls starting with these prefixes are matched against the index. |
externalDomains |
string array | URL prefixes for external APIs. Outbound calls starting with these prefixes are ignored. |
graphqlSchemas |
string array | Printed GraphQL SDL files that define the operations this service serves, relative to carrick.json. Globs are allowed. See Code-first GraphQL schemas. |
Outbound call classification
When code constructs a request URL dynamically (such as fetch(${process.env.ORDER_SERVICE_URL}/orders)), static analysis cannot automatically determine whether the destination is an internal service or an external vendor.
Classifying these variables ensures accurate contract validation:
- Environment variables: Match dynamic URL patterns constructed with template literals or string concatenation.
- Domains: Match explicit URL literals and prefixes.
Calls constructed from unclassified environment variables generate configuration suggestions in pull request comments until they are added to internalEnvVars or externalEnvVars.
Monorepo service configuration
When a repository contains multiple services, declare them using the services array at the top level of carrick.json. Carrick scans, type-checks, and indexes each entry as an independent service, evaluating contracts between services within the monorepo:
{
"services": [
{
"serviceName": "api",
"directory": "services/api",
"include": ["packages/shared"],
"tsconfig": "tsconfig.json",
"internalEnvVars": ["WORKER_URL"]
},
{
"serviceName": "worker",
"directory": "services/worker",
"include": ["packages/shared"]
},
{
"serviceName": "web",
"directory": "apps/web",
"tsconfig": "tsconfig.json",
"externalDomains": ["https://api.stripe.com"]
}
]
}
Service entry fields
| Field | Type | Description |
|---|---|---|
serviceName |
string | Unique service identifier. Always provide an explicit name for each service in a monorepo to avoid index collisions. name is accepted as an alias. |
directory |
string | Path to the service root directory, relative to carrick.json. |
include |
string array | Additional directories containing shared source code or libraries to include during type resolution. |
tsconfig |
string | Path to the service’s tsconfig.json, relative to directory. For Deno services, omit this to use the nearest deno.json or deno.jsonc. |
graphqlSchemas |
string array | Printed GraphQL SDL files that define the operations this service serves, relative to carrick.json. Globs are allowed. See Code-first GraphQL schemas. |
Each entry in the services array also supports the classification fields (internalEnvVars, externalEnvVars, internalDomains, externalDomains). Files outside declared service directories, node_modules, build output directories, and test files are excluded from indexing. When services is present, any sibling top-level flat fields are ignored. An explicit carrick.json takes precedence over the services Carrick derives from workspace manifests.
An explicit ordinary TypeScript config in tsconfig selects the TypeScript path for a Deno service. An explicit Deno config must name the nearest Deno manifest, and deno.json takes precedence over deno.jsonc when both exist. Import maps must be local files.
The optional top-level includes map declares classification for a shared source root once. See Declaring a shared root once.
Declared and routeless operations
Serverless functions (such as AWS Lambdas or Cloudflare Workers) often route requests based on an event payload field or HTTP header rather than an infrastructure path. To index these routeless or dispatching handlers as concrete operations, declare an operations block:
{
"operations": [
{
"service": "orders",
"route": "POST /v1/orders",
"dispatch": { "location": "body", "field": "action" },
"operations": [
{ "value": "create", "handler": "src/create.ts:handleCreate" },
{ "value": "refund" }
]
}
],
"services": [
{ "serviceName": "orders", "directory": "functions/orders" }
]
}
Operations block fields
| Field | Type | Description |
|---|---|---|
service |
string | Target service matching a declared serviceName. |
route |
string | The route served by the handler ("POST /path"). |
dispatch.location |
string | Dispatch field source: "body" or "header". |
dispatch.field |
string | The property or header name that determines dispatch. |
operations[].value |
string | An accepted dispatch value representing a distinct operation. |
operations[].handler |
string | Optional "file:symbol" mapping to navigate directly to the specific handler function. |
Declaring a shared root once
When several services reach a third-party API through the same shared directory, the calls belong to that directory, not to each service that pulls it in. The optional top-level includes map declares classification per shared source root:
{
"includes": {
"packages/shared": {
"externalEnvVars": ["STRIPE_API"],
"externalDomains": ["https://api.stripe.com"]
}
},
"services": [
{
"serviceName": "api",
"directory": "apps/api",
"include": ["packages/shared"]
}
]
}
Each key is a source root spelled as a service names it in its include (a leading ./ and a trailing / are ignored when matching). Each value takes the same four classification fields a service takes.
Every service whose include lists that root inherits those declarations, unioned with its own. A service keeps everything it declares itself, and a name declared in both places appears once. A service that does not include the root inherits nothing.
A key that no service lists in its include fails the scan rather than doing nothing, so a mistyped root is reported instead of leaving the calls unclassified.
Code-first GraphQL schemas
Carrick reads a GraphQL server’s operations from SDL, meaning .graphql and .gql files under the service’s own directory and gql template literals. A schema built in code (Pothos, TypeGraphQL, Nexus) has no SDL in source, so its queries and mutations are not indexed until the service names the printed schema:
{
"services": [
{
"serviceName": "api",
"directory": "apps/api",
"graphqlSchemas": ["apps/api/dist/schema.graphql"]
}
]
}
Each entry is a path relative to carrick.json, or a glob such as packages/schema/generated/*.graphql. The file can sit anywhere in the repository, including a build folder like dist/ or another app’s directory, as long as it is committed. Every Query, Mutation and Subscription field it defines is indexed as an operation that this service serves. A single-service carrick.json accepts the field at the top level.
An entry that matches no file, or a file that defines no root field, is reported as a warning in the scan output. When a service depends on a GraphQL library and serves HTTP routes but indexes no GraphQL schema fields, the scan output suggests this setting.
Once the schema’s fields are known, Carrick also reads the modules that build the schema. These are files that call through a builder value created from a library the scan detects, including field modules that export nothing and only import the builder. Each such file is analysed with the schema’s field list, and a field whose resolver is found there is indexed at the resolver’s line rather than at the printed schema. A field with no located resolver stays at its schema line.
GraphQL documents for another team’s API
A client’s GraphQL documents are indexed as calls only when they are written against a schema this repository serves. Carrick attributes each document (a .graphql or .gql file, or one gql template) to the committed schema file that holds its root fields. A schema file counts as served when a service names it in graphqlSchemas, or when it sits under a service’s directory and that service shows it serves a schema, by serving HTTP routes or by a resolver in its code linked to one of the schema’s fields.
Any other committed schema file marks its documents as calls to an external API, and they are not indexed as calls. That includes a vendor schema a codegen step downloaded into dist/, or a copy committed inside a client app’s own src/. The fields of a copy that sits under a service with no such evidence are not indexed as that service’s operations. The scan output names the schema file and counts the operations, so a schema that is served here but not declared can be added to graphqlSchemas.
Some documents are left out as well:
- A document whose fields no single schema holds.
- A document whose fields sit in both a served and an external schema, when the file’s environment reads don’t settle it. A file that reads only variables from
internalEnvVarscounts as internal, and one that reads onlyexternalEnvVarscounts as external.
A document whose fields appear in no schema the repository holds stays a call, because its server may be another repository in the project.
Mixed dependency example
The following configuration demonstrates a service that interacts with internal microservices, an external payment gateway configured via environment variables, and third-party webhooks:
{
"serviceName": "checkout",
"internalEnvVars": [
"ORDER_SERVICE_URL",
"INVENTORY_SERVICE_URL",
"USER_SERVICE_URL"
],
"externalEnvVars": [
"STRIPE_API_BASE"
],
"externalDomains": [
"https://api.sendgrid.com",
"https://hooks.slack.com"
]
}
Related
- Quickstart walks through generating configuration files during onboarding.
- PR comments describes automated feedback on unclassified environment variables.
- What Carrick covers details framework extraction and routing support.