fix(cli): abort on corrupt config.json instead of falling through to mainnet defaults - #817
Closed
plind-junior wants to merge 2 commits into
Closed
fix(cli): abort on corrupt config.json instead of falling through to mainnet defaults#817plind-junior wants to merge 2 commits into
plind-junior wants to merge 2 commits into
Conversation
plind-junior
added a commit
to plind-junior/gittensor
that referenced
this pull request
Apr 28, 2026
entrius#845) When `~/.gittensor/config.json` failed to parse, `config_set` printed a yellow warning, set `config = {}`, then wrote a fresh single-key file — silently destroying every previously-configured key (network, contract_address, ws_endpoint, hotkey, ...). Mirror PR entrius#817's read-side fix: `JSONDecodeError` now aborts with a clear message and `SystemExit(1)`, leaving the corrupt file untouched so the operator can inspect or recover it. New regression tests in `tests/cli/test_config_set.py` cover the abort-with-nonzero-exit, byte-for-byte preservation of the corrupt file, non-regression of the valid-file merge path, and the first-run case. Closes entrius#845
8 tasks
…mainnet defaults
`load_config` and `_load_config_value` swallowed `json.JSONDecodeError` and
returned `{}` / `None`, silently retargeting commands at finney mainnet and
the hardcoded contract address. A briefly-corrupt config (interrupted
`gitt config set`, manual edit typo, host crash mid-write) would let the
next `gitt vote ...` or `gitt issues register ...` re-target mainnet
without warning.
Now: `JSONDecodeError` aborts with a clear message; `IOError` / `OSError`
(missing or permission-denied) still falls through to defaults, since
those are legitimate "no config yet" states.
entrius#845) When `~/.gittensor/config.json` failed to parse, `config_set` printed a yellow warning, set `config = {}`, then wrote a fresh single-key file — silently destroying every previously-configured key (network, contract_address, ws_endpoint, hotkey, ...). Mirror PR entrius#817's read-side fix: `JSONDecodeError` now aborts with a clear message and `SystemExit(1)`, leaving the corrupt file untouched so the operator can inspect or recover it. New regression tests in `tests/cli/test_config_set.py` cover the abort-with-nonzero-exit, byte-for-byte preservation of the corrupt file, non-regression of the valid-file merge path, and the first-run case. Closes entrius#845
plind-junior
force-pushed
the
fix/load-config-silent-json-error
branch
from
April 29, 2026 20:34
b795812 to
e838b7f
Compare
Collaborator
|
Closing. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
load_config()ingittensor/cli/issue_commands/helpers.pyand the parallel_load_config_value()ingittensor/cli/miner_commands/helpers.pyboth catchjson.JSONDecodeErrorand return{}/Nonewith no log line, no warning, no exit code. Every command that reads~/.gittensor/config.jsonthen proceeds as if the operator had configured nothing.This is a problem because the silent-empty-config path collapses to mainnet:
resolve_network()walks through CLI arg → config → env → default; on empty config the chain ends atNETWORK_MAP['finney'](gittensor/cli/issue_commands/helpers.py).get_contract_address()falls through to the hardcoded mainnetCONTRACT_ADDRESSingittensor/constants.py.So a config file that exists but parses badly silently re-targets every CLI hot path at finney + mainnet contract.
Location
gittensor/cli/issue_commands/helpers.py(theload_config()body):Same shape in
gittensor/cli/miner_commands/helpers.py::_load_config_value(). There are at least 8 commands (vote,register,harvest,view,submissions,predict, minerpost, minercheck) that go through one of these two functions.How the file becomes briefly invalid
The fix should defend against any of these — they're all real:
gitt config set. The current writer is non-atomic (Path.write_texttruncates first), so a Ctrl-C / OOM / host shutdown mid-write leaves the file partial. (This is the write-side half — see C6.)~/.gittensor/config.jsonin$EDITOR, mistypes a comma or a quote, saves.scp,rsync --partial, sync-conflict tools, dotfile managers can produce a half-file at the destination.fsync.Path.write_textdoesn'tfsync— power loss within the writeback window leaves zeros / truncation.chmod 0on the config doesn't reach this branch (raisesIOError, see below) — but a file that exists and is read-protected by a UID change while the validator is running still hits the broken path.The point is that the operator did configure the CLI; the fall-through pretends they didn't.
Why it's load-bearing
The CLI hot path includes operator-only commands:
gitt vote solution <issue_id> <solver_hotkey> <solver_coldkey> <bounty>andgitt issues register --repo … --bounty …. These are typically run from automation (cron, scripts), where:print_network_header(...)prints the resolved network and contract, but it's one line in a Rich panel; non-interactive runs and CI output capture often miss it.confirm_or_abort(...)auto-skips on non-TTY, so the "are you sure?" guard isn't a backstop.There's no recovery from a vote committed to the wrong network.
Distinction from related findings
config setwhitelist) — write-side input validation, doesn't touch reads.config setparse-then-wipe + non-atomic write) — write-side; closes the most common path into the corrupt state. Even after C6, paths 2–5 above still produce corrupt files, and C7's silent-read path still triggers.So the read-side bug is independently exploitable and survives the fix to the write side.
Fix
On
JSONDecodeError, abort the CLI with a clear message that points at the file and instructs the operator to inspect or remove it. Do not return{}/None.IOError/OSError(file genuinely unavailable — missing, permission-denied) is fine to treat as "no config" — those are legitimate "operator hasn't configured this yet" states. The asymmetry is the point: a file that does not exist is "no config"; a file that exists and parses badly is operator data corruption and must not be silently ignored.Suggested message: name the path, name the parse error, instruct the operator to inspect/remove, and exit non-zero. Roughly five lines added per call site.
btcli precedent
btcli upstream PR latent-to/btcli#800 ("fix: JSON output empty for
btcli subnets list --json-out") was caused by the same shape — an exception was silently swallowed and produced empty output instead of an error. btcli's ownConfig.__init__reads YAML and lets parse errors propagate with notry/exceptaround the load. Gittensor's CLI is strictly more silent than btcli on the same surface.Dup-check
Searched closed + open issues and PRs for:
load_config,JSONDecodeError config,config silent,resolve_network finney fallback,mainnet fallback,config in:title. The closest neighbour is #623 (refactor; doesn't address behaviour). No issue currently claims the silent fallback is a correctness bug.Fixes #816
before:

after:
