shuck

Contracts

Shuck tries to learn as much as it can from real shell source before you add any extra configuration.

When a file sources another file, Shuck parses that helper and summarizes the functions, bindings, and reads it contributes. When zsh plugin resolution can find a real plugin or theme entrypoint, Shuck imports that file through the same source-closure pipeline. In other words, Shuck prefers concrete shell files over hand-authored metadata whenever it can recover the behavior directly from source.

That automatic path covers a lot:

  • sourced helpers that define functions or initialize variables;
  • static zsh plugin and theme entrypoints that resolve to real files;
  • explicit plugin loads, theme loads, or raw entrypoints added through config or CLI flags.

Sometimes the source is still not enough.

Common cases include:

  • runtimes or CI systems that provide names the script reads, but that are not assigned anywhere in the repo;
  • plugin or framework code that consumes settings from the requesting file, such as ZSH_TMUX_* options in .zshrc;
  • framework conventions where one file assigns metadata and another dispatcher consumes it later;
  • dynamic loaders, eval, generated names, or plugin-manager DSLs that prevent exact recovery from source alone.

That is where Contracts come in.

Contracts let you add the missing runtime or framework knowledge that Shuck cannot reliably recover from shell source. They enhance analysis; they do not replace normal source closure. Shuck still prefers real sourced files and real plugin entrypoints whenever those are available.

What Contracts can enhance

Contracts can tell Shuck about four kinds of behavior:

  • reads: names that the activated runtime reads;
  • consumes: assignments that remain live because external runtime behavior observes them later;
  • provides: variables or functions that become available when the contract applies;
  • functions: call-scoped function effects, including reads and variables a provided helper may set.

In practice, that means Contracts can improve:

  • unused-assignment analysis for settings that are consumed outside the current file;
  • uninitialized-variable analysis for names that are provided by a runtime or framework;
  • function and binding visibility when a helper is known to exist even though its implementation is not recoverable from source.

Built-in and custom Contracts

Shuck ships with built-in Contracts for well-known behavior that is stable enough to model in the tool itself.

You can:

  • leave the built-ins enabled;
  • disable specific built-ins or whole built-in groups;
  • replace a built-in contract with a repo-specific version;
  • add fully custom Contracts for project-specific behavior.

All of that lives under [lint.contracts]:

[lint.contracts]
well-known = true
 
[[lint.contracts.custom]]
id = "github-actions-env"
when = "always"
files = [".github/**/*.sh"]
provides = { variables = ["GITHUB_OUTPUT", "GITHUB_ENV"] }
 
[[lint.contracts.custom]]
id = "oh-my-zsh-tmux"
when = { type = "zsh_plugin", framework = "oh-my-zsh", plugin = "tmux" }
files = ["**/.zshrc"]
consumes = { prefixes = ["ZSH_TMUX_"] }

In this example:

  • the github-actions-env contract tells Shuck that matching scripts can read CI-provided names without assigning them locally;
  • the oh-my-zsh-tmux contract tells Shuck that matching .zshrc files may assign ZSH_TMUX_* settings that are consumed by the tmux plugin runtime.

Activation and scope

Contracts apply either:

  • to matching files with when = "always", or
  • at a zsh plugin or theme request with when = { type = "zsh_plugin", ... } or when = { type = "zsh_theme", ... }.

For request-based Contracts, files matches the file that requested the plugin or theme, not the resolved plugin entrypoint. That distinction matters for settings-style Contracts: a zsh_plugin contract that consumes ZSH_TMUX_* keeps assignments in the .zshrc live, not assignments inside the tmux plugin file.

files uses the same glob rules as other per-file settings. In most cases you should keep Contracts narrowly scoped to the files where the behavior really applies.

How Contracts relate to plugin resolution

Contracts are especially useful around plugins, but they are not limited to plugins.

Shuck already uses plugin resolution to bring real entrypoint files into the analysis when it can find them. Contracts cover the behavior around those files that source closure cannot see directly, such as:

  • settings consumed by the plugin runtime from the requesting file;
  • framework-provided names that exist before the plugin entrypoint runs;
  • project-specific conventions layered on top of a built-in framework.

If the behavior is recoverable from a real sourced file, prefer source closure or plugin resolution. If it depends on runtime conventions or "assigned here, consumed elsewhere" behavior, use a Contract.

Next steps

Use the Configuration guide for config-file discovery and precedence, the Zsh Support guide for plugin-resolution setup, and the Settings Reference for the exact [lint.contracts] keys and examples.