100% free and open source · every tool, every host, no licence key · read the licence
Secronyx

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-server

Do 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 --json

The 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 install

lefthook 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-commit

Never 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:

  1. 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.
  2. 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.go and *_windows.go under //go:build constraints. macOS collectors that use cgo need a darwin && !cgo counterpart, otherwise a cross-compiled build fails.
  3. 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".
  4. Register the tool in internal/mcp/tools.go (or the appropriate tools_*.go) with its name, description, input schema, scope and handler. Choose the scope carefully — see below.
  5. Add CLI support in runQuery() in cmd/secronyx/main.go so the tool can be exercised with --query.
  6. Add documentation in cmd/docgen/main.go, and update the tool tables in README.md and 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 into sh -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 IIS site_name could 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/redact and 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

  1. Fork the repository.
  2. Branch from main: git checkout -b feat/my-feature.
  3. Make the change, with tests.
  4. Run lefthook run pre-commit and go test -race ./....
  5. Commit with a conventional message.
  6. 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-commit passes (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.

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.