Configuration trust model¶
ASH reads its configuration from places with different owners. You, the person or pipeline running ASH, control the command line, the environment, and the files outside the repository. The repository being scanned controls every file inside it, including the .ash/.ash.yaml that ASH discovers without being asked. A client of the ASH MCP server controls the files it delivers to the server. ASH sorts configuration into three tiers by owner, and limits what the repository and MCP clients can set for a short list of settings: the ones that grant access, send data off the host, or choose code for a tool to run.
The three tiers¶
| Tier | Where the configuration comes from |
|---|---|
| Operator | Command-line flags, including --config-overrides, --ash-plugin-modules, and --sandbox; the environment, including ASH_PLUGIN_MODULES; a config file outside every git checkout and outside the scanned directory, named with --config or ASH_CONFIG. |
| Repository | Any config file inside a git checkout or inside the scanned directory: the discovered .ash/.ash.yaml and its siblings, a --config or ASH_CONFIG that points into the repository being scanned, and a config file kept in a checkout of its own, such as an ops or dotfiles repository. |
| MCP client | Any file under the MCP server's workspace root, where delivered source trees and uploads are stored, except each session's config/ directory, which only the server writes. Also any other config file a client names as a tool's config_path, wherever it is, unless it is in a directory ASH_MCP_ALLOWED_CONFIG_ROOTS names or in its session's config/ directory, which only the server writes. |
The MCP workspace root is ASH_MCP_WORKSPACE_ROOT when set, read against the current directory when it is relative; else $XDG_CACHE_HOME/ash-mcp when XDG_CACHE_HOME is an absolute path, since a relative one is ignored; else ~/.cache/ash-mcp when HOME is an absolute path. A file there is an MCP-client file whether or not a server is running, so a CLI scan of a delivered tree, with that tree as --source-dir, gets the same treatment. With an empty or relative HOME and neither variable, or with an ASH_MCP_WORKSPACE_ROOT under ~, there is no workspace root, and no file is an MCP-client file because of where it is. ash mcp still starts, with a warning that ASH uses nothing under ~.
How a file's tier is decided¶
The tier depends on where a file is, not on who names it. Passing a repository's file with --config does not make it an operator file.
The repository tier exists because the scanned target is untrusted relative to the operator. Nothing the target ships, whether committed or only present in its checkout, may grant itself reads, tool options, or sandbox exceptions. A config file inside any git checkout, the scanned one or a separate one such as an ops or dotfiles repository, may have been written by someone other than the operator, so it is repository-tier, and so is a config file inside the scanned directory when that directory is in no checkout. When ASH ignores a setting from such a file, it names the file in a warning that says how to keep the setting: pass it with --config-overrides, or move it to a config file outside every git checkout and outside the scanned directory.
A file is inside a git checkout when it, or any directory above it, holds a .git entry: a directory, or the .git file of a submodule or linked worktree. ASH checks the path as you named it, its resolved path, and every symbolic link it passes through on the way, so a symbolic link into or out of a checkout counts either way. For a scanned directory that is not in a checkout, a file inside it counts under any of the directory's names: as given, resolved, or as the shell's $PWD names it.
In this example the scanned directory, ../app, is a checkout of its own, and your config is in ../ops, a separate checkout. ASH ignores its sandbox grant, names the setting in a warning, and leaves the grant out of the configuration it uses:
git init --quiet ../app
git init --quiet ../ops
printf 'project_name: app\nsandbox:\n extra_read_paths:\n - /opt/shared-rules\n' > ../ops/ash.yaml
ash config get --source-dir ../app --config ../ops/ash.yaml > ../ops-config.txt 2>&1
grep -F 'sandbox.extra_read_paths' ../ops-config.txt
! grep -F -- '- /opt/shared-rules' ../ops-config.txt
The same file outside every git checkout is the operator's, and the grant is kept:
mkdir -p ../plain
cp ../ops/ash.yaml ../plain/ash.yaml
ash config get --source-dir ../app --config ../plain/ash.yaml | grep -F -- '- /opt/shared-rules'
The rule applies to every file a config is built from, which means the root file and every base it extends, each judged by the path its extends entry names as well as by the file it resolves to. If any of them is inside a git checkout or the scanned directory, the whole config is treated as repository-tier for the restricted settings, and ASH names those files in the warning. An operator file outside every checkout that extends a base inside one keeps the sandbox settings it writes in its own sandbox section, and for some scanners the tool file paths it writes itself, but nothing it inherits from a base. Its other restricted settings come from the trusted base described below, so set them with --config-overrides. A base reached through a symbolic link that leaves the config file's own directory is refused before anything is granted.
ASH reads one config file per scan, plus the files it extends. When you pass --config to a scan of one directory, ASH does not read the repository's own config file. In workspace mode, the file --config (or ASH_CONFIG) names is layered over each project's own config: every setting your file makes is final for every project, and a project's own config fills only the settings yours leaves out (Scan a multi-project workspace). --dry-run shows it in an operator: line under each project. Here the project's config sets LOW and yours sets HIGH, which wins:
mkdir -p ../ws/api
printf 'global_settings:\n severity_threshold: LOW\n' > ../ws/api/.ash.yaml
printf '{"folders": [{"path": "api"}]}\n' > ../ws/project.code-workspace
printf 'global_settings:\n severity_threshold: HIGH\n' > ../operator.yaml
ash scan --workspace ../ws/project.code-workspace --config ../operator.yaml --dry-run > ../plan.txt
grep -F '(the fields it sets are final)' ../plan.txt
grep -E 'threshold: +HIGH' ../plan.txt
When a config is repository-tier, ASH takes the restricted settings from a trusted base instead: the file ASH_CONFIG names, if it is outside every git checkout and the scanned directory, otherwise the defaults, with your --config-overrides applied on top. In workspace mode, the scanned tree is the checkout that holds the workspace root, or the root itself, and the --config file given to the workspace scan is the trusted base when it is outside every git checkout. That file, and every base it extends, must be outside that tree: one inside it, or under the MCP workspace root, is refused with exit code 3, naming the file, before any project is resolved. Under the MCP server, the trusted base is the server's own configuration, the file ASH_CONFIG names, under the same rule: when that file is inside a git checkout or the scanned directory, the restricted settings come from the defaults. An ASH_CONFIG that is unset or empty means the defaults. One that names a path that is not a file, a file that cannot be read, or a config that does not load stops a scan with exit code 3, and stops ash mcp from starting, with exit code 3.
Two setups surprise people. A config you trust but keep in a git checkout of its own, such as an ops repository, cannot make the restricted settings below, because it is in a checkout. If your home directory is itself a git checkout (a common dotfiles setup), the outermost checkout above a project under it is your home directory, so a scan of that project counts your whole home directory as the scanned tree. Every config file under your home directory, yours included, is then repository-tier for that scan. Keep your operator config outside it, for example under /etc/ash.
The scanned tree¶
One rule looks only at the scanned tree: where a tool file the operator names may be. The scanned tree is the git checkout that holds the scanned directory: the outermost directory at or above it that holds a .git entry. When the scanned directory is not in a checkout, the scanned tree is the scanned directory itself. ASH finds the tree from the scanned directory as you named it, from its resolved path, and, when you scan the working directory or one below it, from the shell's $PWD. A file is inside the tree when it or a directory above it is the tree's directory, compared by file identity rather than by name, and a symbolic link inside the tree counts as inside it wherever it points.
A tool file you name as the operator, with --config-overrides, in a config file outside every git checkout and the scanned directory, or in an environment variable, is used unless it is inside the scanned tree and the scanner does not allow one there, so it may be in a checkout of your own, such as an ops repository. A tool file that any other config file names, one in an ops checkout included, or that ASH finds by name, is used only from outside every git checkout and the scanned directory. To keep such a path, move the config file outside every git checkout and the scanned directory, or pass the option with --config-overrides. Options only the operator can set lists the scanners this covers. ASH names each refused path in a warning of this form, where <option> is the option or the file it looked for, and <reason> says where the file is, such as it is inside the git checkout at /home/me/ops:
Ignoring <option> (<path>): <reason>. A file that a config file inside a git checkout or the scanned directory names, or that ASH finds in the scanned directory, is passed to the tool only from outside every git checkout and the scanned directory. To keep a setting, move the file outside every git checkout, or pass the setting with --config-overrides. A file inside the scanned directory has to move out of it as well.
For a path you named, the warning ends instead: "A file inside the scanned tree, or one an MCP client delivered, is not passed to the tool; name one outside it."
What each tier may set¶
Settings not listed here can be set from any tier. The repository tier's limits also apply to MCP-client files, which have further limits of their own (next section).
| Kind of setting | From the operator | From a repository file |
|---|---|---|
Sandbox grants: sandbox.network_scanners, sandbox.extra_read_paths, sandbox.read_path_scanners, and sandbox.env_scanners |
Honored | Ignored. A network_scanners list is kept as a limit: scanners it does not name get no network. |
The sandbox mode, sandbox.mode |
Honored. A mode in ASH_CONFIG's file, or in the --config file of a workspace scan, counts even when that file is inside a git checkout or the scanned directory, because a mode other than off grants nothing. |
Applies only when no operator source turns the sandbox on. It cannot turn off, or switch away from, a mode the operator chose. In workspace mode this includes a project's own config file. |
Plugin module lists, ash_plugin_modules |
Honored | Only ASH's own plugin modules and modules installed outside every git checkout and the scanned directory. A third-party package in a virtual environment inside a checkout counts as inside it. Never ash_aws_plugins or ash_snyk_plugins, or any module inside either, whose plugins send findings or source off the host. |
Where the AWS reporters send results, and with which credentials: the region, profile, and account of aws-security-hub; the region, profile, model, and output files of bedrock-summary-reporter; the region, log group, and stream of cloudwatch-logs; the region, profile, bucket, and key prefix of s3; and whether each of these reporters runs |
Honored | Replaced by the trusted value. |
Whether the scanned repository's own .gitignore and .ignore files decide what is scanned, honor_ignore_files |
Honored | Replaced by the trusted value. |
Letting trivy send anonymous usage data to its vendor on an online trivy-repo scan, scanners.trivy-repo.options.disable_telemetry: false |
Honored | Replaced by the trusted value: the default true, which passes trivy repository --disable-telemetry, unless the operator set false. A file may set true. |
The archive extraction limits, max_members, max_member_bytes, max_total_bytes, and max_compression_ratio under converters.archive.options |
Honored | A value at or below the trusted one is kept; a higher one is replaced by the trusted value. |
detect-secrets' verification filter, which sends each candidate secret to the service that issued it, and plugins or filters it loads from a file:// path |
Honored, except from a baseline inside the scanned tree | Removed, unless the trusted settings name the same filter or file. A baseline's own copies of these count only when the baseline is outside the scanned tree and any checkout that holds it, and an operator source names the same baseline_file. |
Which release of a scanner's or converter's tool runs, tool_version, in every scanner and converter section |
Honored | Replaced by the trusted value, or by the tool's default constraint where the trusted settings set none. |
Relaxing the stale content database check: content_db_staleness: warn, and content_db_staleness_overrides entries with policy: warn |
Honored, as is --allow-stale-content-db |
A value equal to the trusted one, or one that makes the check stricter (fail), is kept. Any other policy is replaced by the trusted value, and any other entry is dropped. |
| Scanner options that name a tool config file, ignore file, rules or plugin directory, or a program for a tool to run, or that choose which part of a tool's own config applies | Honored. For some scanners the file may not be inside the scanned tree. | Limited for some scanners only: some ignore the repository's value, and the others accept only a path outside every git checkout and the scanned directory. Set these options from an operator source, and pass your config with --config so the repository's config is not read. |
Options only the operator can set lists the scanners that limit the options in the last row. The reporter limits apply to a reporter's section under any spelling ASH reads as that reporter, so a repository's reporter section spelled BedrockSummary is limited like bedrock-summary-reporter. The table is how a scan reads the reporter settings. ash report and ash merge read stored results, which do not record which tree was scanned, so any config file they read, even one outside every git checkout, is held to the repository column for whether each AWS reporter runs and where it sends: those come only from --config-overrides (Rendering a reporter from saved results). Two rules apply from every tier: reporters.<name>.extension must be a plain filename suffix, and report files are written only inside the reports directory.
When a lower tier sets something it may not¶
From a repository file, ASH ignores the value, uses the trusted one, logs a warning that names the settings and the file and says how to keep them, and runs the scan. For example:
Ignoring sandbox.network_scanners, sandbox.extra_read_paths from /work/repo/.ash/.ash.yaml: that config is inside a git checkout or the scanned tree, or was delivered by an MCP client. A network_scanners list there still removes network from scanners it does not name, and a sandbox mode there still applies when nothing else turns the sandbox on. To keep a setting, move the file outside every git checkout, or pass the setting with --config-overrides. A file inside the scanned directory has to move out of it as well.
Here the repository's config gave grype a network and a read path. To give grype a network yourself, pass --config-overrides 'sandbox.network_scanners=[grype]'.
An MCP-client file is also checked against the server's runtime-override policy, the denied_paths and denied_value_patterns of global_settings.mcp.runtime_overrides, whether or not runtime_overrides.enabled is set. The policy comes from the server's own configuration: the file ASH_CONFIG names when the server starts, or the built-in defaults when ASH_CONFIG is unset or empty. An ASH_CONFIG that names a path that is not a file, or a file that does not load, stops ash mcp from starting, with exit code 3. By default the list covers the gate settings (fail_on_findings, fail_on_incomplete_scanners, content_db_staleness and its overrides, and content_db_staleness_offline_image_default), the scanner options that drop findings by rule ID, by a baseline, or by a whole category, options that limit how much a scanner reads or that turn on its own network calls, each scanner's offline option, each scanner's own config file, which version of a tool runs, every setting of the reporters that send findings off the host (aws-security-hub, bedrock-summary-reporter, cloudwatch-logs, and s3), their enabled included, and the gitlab-sast reporter's exclude_suppressed; What a delivered config cannot set lists them. If a delivered file sets any of them to a value other than the trusted one, ASH refuses the whole file rather than trimming it. The error names each field, the MCP tools report it with error_type: config_field_denied, and on the CLI it is a configuration error (exit 3). If the policy itself cannot be read, because the file ASH_CONFIG names exists but does not load, or has two sections that ASH reads as the same plugin, a config with an MCP-client file is refused with config_policy_unreadable. The server meets that case only when the file changed after it started.
The check follows where the file is, not whether a server is running. In this example the parent directory is the workspace root, so the scanned directory is a delivered tree, and its config turns off fail_on_findings:
The CLI refuses the config with exit code 3:
ASH_MCP_WORKSPACE_ROOT="$(dirname "$PWD")" ash --source-dir . --scanners detect-secrets --offline --no-progress
Outside the workspace root the same file is a repository file, which may turn the gate off, so the same scan exits 0 although detect-secrets reports a finding:
The default list also names sandbox, ash_plugin_modules, global_settings.mcp, suppressions, and ignore paths, and delivered files are checked differently for those. Sandbox settings and plugin modules may restrict the server's own but not widen them: a delivered file whose own sandbox section gives a scanner a network or an extra_read_paths entry the server's config does not, or sets a sandbox mode other than the server's when the server's mode is not off, or whose own ash_plugin_modules list loads a module the table above keeps out, is refused the same way. Any other sandbox grant or module a delivered file asks for is dropped, as for a repository file. A delivered file's global_settings.mcp section has no effect, and suppressions and ignore paths are honored and marked, as described next. Under the default policy a delivered config can still disable scanners, turn the reporters that write only local files on or off and set their options, apart from gitlab-sast's exclude_suppressed, and raise severity thresholds. It cannot turn a reporter that sends findings off the host on or off: its enabled: false for one of them is not refused, but it has no effect, and each runs as the server's config says, or by the reporter's own default where that config sets nothing. Run the MCP server covers running the server.
Client-supplied suppressions and ignore paths¶
In a scan whose config includes an MCP-client file, each suppression and ignore path that file sets is marked client_supplied: true, unless it matches an entry from an operator source. Per-scanner path exclusions that file sets (bandit excluded_paths, checkov skip_path, semgrep and opengrep exclude, syft exclude, and ferret-scan exclude_patterns) are taken out of the scanner's options and handled the same way. Each is matched the way its scanner reads it: a bandit entry as the **/<path>/** glob ASH passes bandit, against the file's absolute path; a checkov entry as a regular expression found in, or a substring of, the absolute path of the file or of a directory above it; and the others as globs that match the path or any directory in it. A finding the matching misses is reported unsuppressed, never hidden. A delivered scanners.trivy-repo.options.ignore_file is not passed to trivy. When it is a regular file inside the scanned tree, each unexpired ID in it suppresses and marks the trivy-repo findings with that rule ID; any other file, including a symbolic link, is not read.
The mark changes what the entry does. A client-supplied ignore path or exclusion does not keep files from being scanned or converted. The files are scanned, and each finding the entry would have hidden is kept in the results as a suppressed finding. Like every suppressed finding, it does not count toward fail_on_findings.
The mark shows up in:
ash_aggregated_results.json, on each entry ofash_config.global_settings.suppressionsandignore_paths;- the SARIF, where the suppression's justification starts with
(ASH, client-supplied config)and, for a scanner exclusion, names the entry it came from, such asscanners.bandit.options.excluded_paths entry 'tests', and its properties carryclientSupplied: true. A suppression entry haskind: inSource; an ignore path or scanner exclusion haskind: external; - the reports that show suppression justifications: CSV and flat JSON, HTML, JUnit (
<skipped>), OCSF, and GitLab SAST (which keeps suppressed findings at severity Info unlessexclude_suppressedis set).
The GitHub code scanning SARIF leaves suppressed findings out by default, and the Markdown and text summaries show only counts, so the mark is not visible there. Syft's SBOM components get an ash:clientSuppliedExclusion property instead. Each exclusion taken out of a scanner's options is also logged as a warning and recorded in the results as a config_warning validation checkpoint.
ASH marks entries only in MCP-client files and, in an ash --workspace scan, in a workspace policy file under the MCP workspace root, whose entries are also checked against the runtime-override policy as each project gets them. The MCP workspace tools do not use such a policy file at all (Workspace scans over MCP). Only ASH sets the mark: a client_supplied value written in a config file, or set with --config-overrides, is ignored. In a CLI scan, a repository's own suppressions look the same as yours.
What the trust model does not protect against¶
The tiers decide who may change the settings above. They do not make every setting the operator's, and they do not make scanning a hostile repository safe on their own.
-
A repository-tier config on the CLI can still turn off
fail_on_findingsandfail_on_incomplete_scanners, raise severity thresholds, disable scanners and reporters, and add suppressions, ignore paths, and per-scanner exclusions. None of this is marked. To keep these choices yours in a scan of one directory, pass your own config with--config, so that the repository's config is not read. You can also override the repository's values on the command line:--config-overridesreplaces any setting (for examplescanners.bandit.enabled=trueorglobal_settings.suppressions=[]), flags such as--fail-on-findings,--fail-on-incomplete-scanners, and--no-allow-stale-content-dbtake precedence over a config file, and--ignore-suppressionsstops the repository's suppressions and ignore paths from applying. In workspace mode every setting your--configmakes is final for every project, and a project's own config fills only the settings yours leaves out.With
--ignore-suppressions, every scanner reads the files under the repository's ignore paths, and their findings come back. It does not lift a scanner's own exclusion options, such as bandit'sexcluded_paths, or the repository's.gitignoreand.ignorefiles (See every finding). In a repository where detect-secrets reports a password inapp.py, a repository config that ignoresapp.pykeeps the scan passing:--ignore-suppressionsbrings the finding back, and the scan fails:To drop only the repository's ignore paths and keep its suppressions, replace the ignore paths with
--config-overrides, and the finding fails the scan too:ash --source-dir . --scanners detect-secrets --offline --no-progress \ --config-overrides 'global_settings.ignore_paths=[]'A suppression of the same finding is lifted the same way, and the scan fails:
global_settings: suppressions: - path: app.py rule_id: SECRET-SECRET-KEYWORD reason: Fixture value, not a real password -
Some scanners read configuration files from the scanned directory by name. For actionlint, bandit, cfn-lint, checkov, ferret-scan, gitleaks, grype, opengrep, semgrep, syft, trivy, trivy-repo, and zizmor, ASH gives the tool its own configuration instead, or keeps the repository's file from being read, a
.semgrepignoreincluded. detect-secrets reads a.secrets.baselineor.ash/.secrets.baselinein the source directory and uses the plugins and filters it lists, unless your config setsplugins_usedin detect-secrets'scan_settings. The table above says which of the baseline's settings ASH drops. Apart from those settings, ASH does not limit or mark these files, or any other file a tool reads from the scanned directory on its own. - Inline suppression comments in the source, ASH's
ash-ignoreand each scanner's own (for example bandit's# nosec), are written by the repository and honored in every tier. - Only ASH's own AWS reporters and off-host plugin packages are covered. A third-party reporter declares its destination options in the class attribute
destination_options; ASH reads the declaration from the class and admits a destination only from the trusted base: the defaults, an operator config outside the tree, and--config-overrides. A third-party plugin package installed outside every git checkout and the scanned directory, named in a repository config, is imported and runs with ASH's access. - Once you load
ash_aws_plugins, a repository config still sets the Bedrock reporter's content options, such asinclude_code_snippetsandcustom_prompt. A config an MCP client delivers can also turn the reporters that write only local files on or off and set their options. Once you loadash_snyk_plugins, a repository config can switch snyk-code off and set its options. - The MCP-client tier limits what a delivered config can change in a scan. It is not a boundary between MCP clients; see Run the MCP server for the deployment it supports.
Scanning untrusted code covers the other defenses.