Project
Contributing
How to build, test and submit changes to secronyx, including the lefthook quality gates, the conventional commit format semantic-release depends on, and the cross-platform and security rules a new collector must satisfy.
Secronyx is developed in the open at github.com/levantar-ai/secronyx: public source, public issues, public releases. Contributions are accepted through pull requests from forks. This page is the practical guide — what to install, what the hooks check, what the reviewers check, and the rules that are specific to a read-only diagnostics agent and not obvious from the Go conventions alone.
Note that the project is free and open source under the AGPL-3.0, and that contributions are covered by a Contributor License Agreement you will be asked to sign before your first pull request is merged. Read Licensing before you spend significant time on a change.
Development setup
You need Go and Git, plus Make if you want the convenience targets. CONTRIBUTING.md in the repository states Go 1.21 or later; the module itself declares go 1.23.0 with a toolchain go1.24.4 directive, and CI builds with Go 1.22, so install at least 1.23 to build the module as it stands.
git clone https://github.com/levantar-ai/secronyx.git
cd secronyx
go build -o secronyx ./cmd/secronyx
go build -o secronyx-token-server ./cmd/secronyx-token-serverDo not let your local toolchain rewrite the go directive in go.mod. A newer Go will bump it on go get, which then breaks the CI and container builds that pin an older version.
The quickest way to exercise a change without an MCP client in the loop is direct query mode, which bypasses the protocol entirely:
./secronyx --query get_cpu_info --json
./secronyx --query get_capabilities --pid 1 --jsonThe full set of flags is in the Command line and Configuration reference pages.
Tools the hooks expect
Install these before your first commit; the hooks invoke them by name.
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install github.com/securego/gosec/v2/cmd/gosec@latest
# then install lefthook by whatever route suits your platform, and run:
lefthook installlefthook install writes the Git hooks defined in lefthook.yml. Install them; do not work around them.
Quality gates
lefthook.yml defines two stages. pre-commit runs in parallel over staged *.go files:
| Command | What it runs |
|---|---|
go-fmt |
gofmt -l on the staged files; fails if any file is unformatted |
go-vet |
go vet ./... |
go-lint |
golangci-lint run ./... |
go-sec |
gosec -quiet ./... |
pre-push runs the heavier checks:
| Command | What it runs |
|---|---|
test |
go test ./... -count=1 |
full-lint |
golangci-lint run --enable=staticcheck ./... |
security-scan |
gosec -quiet ./... |
Run the pre-commit set yourself before committing:
lefthook run pre-commitNever use --no-verify to get past a hook. If a hook is wrong, fix the hook in a separate change and say so; if gosec flags something you have judged safe, add a #nosec annotation with a reason on the same line, in the style the codebase already uses:
// #nosec G204 -- intentional for test mocking, args come from test setup
cmd := exec.Command(os.Args[0], cs...)Tests
go test ./... # everything
go test -v -race -coverprofile=coverage.out ./... # what CI runs
go test -v -run TestFunctionName ./internal/cpu/... # one test
INTEGRATION_TEST=true go test -v -tags=integration ./test/integration/...Three rules matter more than the rest.
Race detection is not optional. CI runs unit tests with -race. The server holds its tool registry under a sync.RWMutex and the audit writer, rate limiter and replay caches are all shared across request goroutines; a data race in any of them is a correctness bug in a security control.
Seventy per cent coverage for new code. That is the threshold stated in the pull request checklist. No hook enforces it — pre-push runs go test ./... -count=1 with no coverage threshold — so it is a review expectation. Coverage from the unit and Linux integration runs is merged and reported to SonarCloud in CI.
No test may touch the real system. A test that runs the host's ps or powershell is not a test; it is a report on the machine it happened to run on, and it will behave differently on a CI runner, a developer laptop and a container. Every command goes through internal/cmdexec, so mock it:
func TestGetIPv6Status(t *testing.T) {
cmdexec.UseMocks()
defer cmdexec.Reset()
cmdexec.SetMockOutput("ip", `1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
inet6 ::1/128 scope host
`)
result, err := NewCollector().GetIPv6Status()
// assert on result
}
// Required once per test file that uses mocks.
func TestHelperProcess(t *testing.T) { cmdexec.HelperProcess() }The same principle applies to anything else with a boundary: HTTP clients, filesystem roots, the clock. Inject the dependency as a package-level function variable and replace it in the test. Tests that must observe real system behaviour belong in test/integration/ behind the integration build tag, where CI runs them per platform with -tags=integration,linux, integration,darwin or integration,windows.
Adding a new tool
The steps, in the order that avoids rework:
- Write the test first. Decide the result shape, add it to
pkg/types, and write a table-driven test against mocked command output or fixture files. - Add the collector. Create or extend a package under
internal/. The exported method is platform-neutral and forwards to an unexported implementation; the implementations live in*_linux.go,*_darwin.goand*_windows.gounder//go:buildconstraints. macOS collectors that use cgo need adarwin && !cgocounterpart, otherwise a cross-compiled build fails. - Return empty results, not errors, on unsupported platforms. This is the rule that lets a single tool catalogue be offered on every host. A model that receives an error has to reason about whether the host is broken; a model that receives an empty document knows the answer is "nothing here".
- Register the tool in
internal/mcp/tools.go(or the appropriatetools_*.go) with its name, description, input schema, scope and handler. Choose the scope carefully — see below. - Add CLI support in
runQuery()incmd/secronyx/main.goso the tool can be exercised with--query. - Add documentation in
cmd/docgen/main.go, and update the tool tables inREADME.mdand the documentation site.
Choosing a scope
Every tool carries exactly one scope, and the scope is the whole of the authorization story: it decides whether the tool is registered at all under --scopes, whether it needs --enable-sensitive, and whether an authenticated caller may invoke it. Put a tool in sensitive if its output can contain credentials, key material, account data or anything that would be a finding if it appeared in a chat transcript — environment variables, process environments, auth logs, user accounts, sudo and SSH configuration, certificates. When in doubt, choose sensitive; it is a one-line change to relax it later and a CVE to have got it wrong. Scopes and authorization lists the existing scopes and what is in them.
Security rules for a collector
These are the ones reviewers will hold you to.
- Read-only, always. No writes, no creates, no deletes, no signals, no configuration changes. If a diagnostic genuinely requires mutation, it does not belong in this project.
- No shell. Use
cmdexec.Command(name, args...)with a fixed program name and parameterised arguments. Never build a command string. Never pass user input intosh -c. - Quote everything that reaches PowerShell. Windows collectors that interpolate a caller-supplied value into a script must wrap it in
cmdexec.PSQuote, and should validate it against an allowlist first. The 1.0.0 release fixed a command injection in exactly this shape, where a crafted IISsite_namecould execute commands with the agent's privileges. - Validate caller-supplied names. IIS site and application pool names, Windows event log channels and providers, service names and probe targets are all checked against strict allowlists before use.
- Use the path policy. Any tool that opens a caller-supplied path goes through
internal/pathpolicy, which enforces allowed roots, re-checks the symlink target, requires a regular file, caps the size and denies key material. Do not open a path directly. - Redact new output that can carry secrets. If your collector can surface a connection string, a token, a password field or a command line with credentials in it, route it through
internal/redactand add the field to the redaction coverage inventory. - No recursive scans. There is deliberately no grep, find or recursive filesystem primitive in the tool surface. Do not add one.
Security model, Redaction and Network and path policy describe what these controls do at runtime.
Cross-platform expectations
Provide implementations for all three operating systems where the concept exists. Where it does not — get_iis_sites on Linux, journald on Windows — the tool still registers and returns an empty result.
Use native interfaces before shelling out: /proc and /sys on Linux, sysctl and the native frameworks on macOS, the registry and CIM/WMI on Windows. A file read is faster than a process spawn, cannot be injected into, and does not depend on a utility being installed.
Be careful with cgo on macOS. Several Darwin collectors call native frameworks, which means they are behind //go:build darwin && cgo with a darwin && !cgo fallback. This is why release macOS binaries are built natively on macOS runners rather than cross-compiled, and why go vet -tags=integration,darwin is run separately in CI.
Commit messages
The project uses Conventional Commits, and semantic-release derives every version number from them. A malformed commit message on main does not just look untidy; it produces the wrong release, or no release.
type(scope): description
- type:
feat,fix,docs,refactor,test,chore - scope: the area touched, for example
core,logs,hooks,sbom,mcp,ci - description: imperative mood, lower case, no trailing full stop
feat(sbom): add Ruby gem scanner
fix(core): handle missing /proc/meminfo gracefully
docs: update installation instructions
feat: produces a minor release, fix: a patch release, and a BREAKING CHANGE: footer a major release. Treat a change to a tool's output shape, a flag's name or a default value as breaking. Releases and versioning explains what happens after your commit lands.
Pull requests
- Fork the repository.
- Branch from
main:git checkout -b feat/my-feature. - Make the change, with tests.
- Run
lefthook run pre-commitandgo test -race ./.... - Commit with a conventional message.
- Push and open a pull request.
The pull request template asks you to confirm:
- Commits follow Conventional Commits, because semantic-release depends on it
lefthook run pre-commitpasses (gofmt, go vet, golangci-lint, gosec)go test -race ./...passes and coverage for new code is at or above 70%- New collectors return empty results, not errors, on unsupported platforms
- All command execution goes through
cmdexec.Command()with parameterised arguments - Any new output that can carry secrets is covered by redaction
- Docs are updated where behaviour changed
It also has a Security review section. Answer it properly if your change reads new files, runs new commands, opens network connections, or touches an authentication or authorization path. "N/A" on a change that adds a collector reading a new file is the fastest way to have the review stall.
What CI runs
.github/workflows/ci.yml runs on pushes to main and develop, on pull requests to main, and on manual dispatch. Concurrency is grouped per ref with in-progress runs cancelled.
| Job | What it does |
|---|---|
| Unit Tests | go test -v -race -coverprofile=coverage.out ./... on Linux |
| Integration Tests (Linux) | -tags=integration,linux, 15 minute go test timeout inside a 20 minute job cap, with coverage |
| Integration Tests (macOS) | -tags=integration,darwin, 15 minute go test timeout inside a 20 minute job cap, native runner |
| Integration Tests (Windows) | -tags=integration,windows, 25 minute go test timeout inside a 40 minute job cap |
| Lint | golangci-lint |
| Security Scan | gosec |
| SonarCloud | merged unit and integration coverage |
| Build | five targets: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64 |
| Test Custom Actions | the .NET custom actions used by the Windows MSI |
| Release | semantic-release, only on a push to main |
The build job waits on the three integration jobs, the lint, security and SonarCloud jobs; the release job waits on the build and on Test Custom Actions.
Reporting bugs and asking questions
Blank issues are disabled. Use the bug report or feature request template. The bug template asks for secronyx --version, the platform, the transport (stdio, HTTP, or hybrid/SaaS poller), what happened, and reproduction steps — provide all of them, including the tool name and arguments if a tool call was involved. Usage questions go to GitHub Discussions.
Do not file a security vulnerability as an issue. Follow Reporting a vulnerability instead.
Licensing and the contributor agreement
Secronyx is free and open source under the GNU Affero General Public License v3, an OSI-approved open-source licence: anyone may use, modify and distribute it, including commercially. Running it asks nothing of you. If you distribute modifications, or offer a modified version to users over a network, AGPL asks you to publish those modifications under the same licence.
Before your first pull request is merged you will be asked to sign the Contributor License Agreement. It grants Levantar AI the rights needed to distribute your contribution under the AGPL and to offer commercial licences alongside it, and you keep the copyright in your work.
By opening a pull request you confirm that:
- The contribution is your own work, or you have the right to submit it
- You are happy for it to be distributed under the project's licence
- It contains no code copied from a source with incompatible terms
If you are employed, your employer may own the copyright in code you write. Check that you have the authority to make that grant before you open the pull request, not after. Licensing sets out both documents in full.
Code of conduct
The project adopts the Contributor Covenant. Participation is expected to be harassment-free and constructive; enforcement concerns go to hello@secronyx.com, are reviewed and investigated promptly and fairly, and the reporter's privacy is respected. The full text is in CODE_OF_CONDUCT.md in the repository.
Related pages
Architecture · Licensing · Releases and versioning · Reporting a vulnerability · Command line · Documentation home
Built 2026-09-19. Source: levantar-ai/secronyx. Found a mistake? Tell us.