OpenAI’s Model Spec Rewrite Turns Style Into a Preference and Oversight Into a Root Rule, but the Public Evals Still Test the December Text
A section-by-section diff of OpenAI’s August and October Model Spec releases shows 7,200 words of style and voice guidance retired, a new root-level oversight rule, and a public eval dataset that...
OpenAI published an updated Model Spec on October 29 and described it as a reorganization around five values. The announcement is a short post. The document behind it is a Markdown file of about 40,000 words in a public repository whose release history runs back to February 2025, so the post can be checked against the text instead of taken on trust.
Table Of Content
- The short version
- What OpenAI announced
- How I compared the releases
- Pinned sources
- How the Markdown marks tiers and clauses
- The parser
- The release comparison
- How to read the carry-over numbers
- Finding 1: The Spec kept its size while about 29 percent of its words changed
- How the five values share the page
- Finding 2: The retired style guidance is 7,200 words, and it includes personality and voice
- What the retirement does to the tiers
- Finding 3: A new root-level rule makes corrigibility a Spec requirement
- What the rule says
- The sentence that moved
- The rule it left behind moved down a tier
- Instructed by whom?
- Finding 4: Edits to other root rules are real but not itemized
- Finding 5: The public evals still test the December text
- What OpenAI said the dataset would do
- What the October text does to it
- Which new clauses have no prompts
- The agentic gap is partly by design
- What to do if you build on these models
- What I could not check
- Sources
- Related coverage on sxz.io
I compared the 2026-08-18 and 2026-10-29 releases section by section, then tested OpenAI’s public eval dataset against both. Two short Python scripts and a shared parser module, with every input pinned to a commit hash, reproduce each number below. I ran no model. This is an audit of documents, and OpenAI itself says its models “do not yet fully reflect” the Spec.
The short version
- Same size, different text. The Spec lost 392 words (1.0 percent), but about 29 percent of its words sit in sentences that exist in only one of the two releases.
- The retired style guidance is 7,200 words. That is 20 headings and 17.6 percent of the August text, and it covers five personality traits and the whole voice-mode section as well as tone and formatting.
- A new root-level rule. “Support human oversight and correction” says GPT “must remain corrigible,” a word that appears in no earlier release. The announcement mentions oversight twice and does not link the rule.
- A rule slid down a tier. “Control and communicate side effects” went from root to user level, and its sentence about treating side effects as real during simulations and evaluations moved into the new root rule.
- Other root rules were edited. A scope’s ending condition went from “must” to “should,” a delegation duty gained “unless instructed otherwise,” the always-prohibited list changed, and a tie-break for conflicting root principles disappeared. The changelog calls the remainder “a few other small changes and copy edits.”
- The public evals test the old text. The dataset is built for the Spec “as of 2025-12-18.” Against the October text, 20.5 percent of its prompts are keyed to clause tags that no longer appear, and none of the 103 clauses tagged since August has a prompt.
What OpenAI announced
The announcement says: “The update reorganizes its guidance around five underlying values: faithfulness to human intent, harm minimization, honesty and transparency, human agency, and care.” It says the revision “adds guidance on context, oversight, and agency, adjusts some behavioral defaults, and retires much of the detailed style guidance.” Later it says OpenAI is “retiring most of the Spec’s detailed style guidance,” and it explains the rename from “the assistant” to GPT as “reflecting the increasing breadth of our models’ use cases and deployment settings.” The caveat comes near the end: “The Model Spec describes the behavior we are working toward; our models do not yet fully reflect it.”
The project’s changelog is more specific. It lists the five values, the rename, new principles on user ability and social norms, a new section called “Consider context carefully,” and the sentence “Adds explicit guidance on supporting human oversight and correction.” It also says “Removes most detailed style guidance” and closes with “A few other small changes and copy edits.” Everything else has to come from the text, which is what the rest of this piece does.
How I compared the releases
Pinned sources
- Spec text:
model_spec.mdin github.com/openai/model_spec at two release commits: 7f1cf79, titled “Release 2026-08-18,” and 7ab1b12, titled “Release 2026-10-29” and committed at 06:52 UTC on October 29. The scripts also read the five earlier releases, back to 2025-02-12, for one vocabulary check. - Eval dataset: openai/model_spec_dataset at commit 89c2607, committed on March 24, 2026. As of October 31 it is the only commit in that repository.
- Eval harness: openai/model_spec_evals at commit 1d5df6d, also committed on March 24 and also its only commit.
- OpenAI’s own descriptions: the Spec announcement, the March 25 post Model Spec Evals, and the October 29 post How we detect frontier model misalignment.
How the Markdown marks tiers and clauses
Each section heading in the Markdown carries a tag such as {#avoid_sycophancy authority=user}: a stable id and an authority tier. The tiers decide who can change a rule. A root section’s instructions “can only come from the Model Spec, and cannot be overridden by conversational context.” (August worded it as “cannot be overridden by system messages, developers or users.”) User-level instructions “are defaults that can be overridden only by explicit instructions at the user level or higher.” Guideline-level defaults can be overridden implicitly, and the Spec says “we prefer to place as many instructions as possible at this level.” Moving a rule between tiers therefore changes who can switch it off.
Individual clauses carry footnote ids such as [^agc2]. The public rendering strips them, but they matter because the eval dataset uses them. Its README says “focus_id corresponds to the focus area found in the model_spec.md file in the form [^xxxx].” That mapping checks out: 223 of the dataset’s 225 focus ids appear as tags in the August text.
The parser
This module downloads a pinned release, splits it at headings, and records each section’s id, tier, and word count. It uses only the standard library and works on Python 3.10 or newer.
# specparse.py
# fetch pinned Model Spec releases and split them into sections.
import re
import urllib.request
from functools import lru_cache
# Release commits in github.com/openai/model_spec (the commit message is "Release <date>").
RELEASES = {
"2025-02-12": "fc9366e035db712509824eb8b175c26cf5fdf45b",
"2025-04-11": "095dabb04dd2ade5e19d4438cec9a329ad967e02",
"2025-09-12": "49e51d7fbe0c210d4c37718e645baa5e9b3464b3",
"2025-10-27": "bda3e4c19b46a703f61b71d65023a07d8751dec5",
"2025-12-18": "8d2405d4e9a5143c5355f50482394ebd12fe898c",
"2026-08-18": "7f1cf79fcb656c07f77c8d95b6fbc78dc7fac5b6",
"2026-10-29": "7ab1b124b10b56389641761014823346f1063a6a",
}
def get(url):
request = urllib.request.Request(url, headers={"User-Agent": "model-spec-audit/1.0"})
with urllib.request.urlopen(request, timeout=90) as response:
return response.read()
@lru_cache(maxsize=None)
def spec_text(release):
sha = RELEASES[release]
return get(f"https://raw.githubusercontent.com/openai/model_spec/{sha}/model_spec.md").decode("utf-8")
HEADING = re.compile(r"^(#{1,6}) (.*?)(?:\s*\{#([^}]+)\})?\s*$")
def sections(text):
"""One dict per heading: level, title, id, authority tier, own text and own word count."""
found = []
for line in text.splitlines():
match = HEADING.match(line)
if match:
attributes = match.group(3) or ""
tier = re.search(r"authority=(\w+)", attributes)
found.append({
"level": len(match.group(1)),
"title": match.group(2),
"id": attributes.split()[0] if attributes else match.group(2).lower(),
"tier": tier.group(1) if tier else "none",
"lines": [],
})
elif found:
found[-1]["lines"].append(line)
for section in found:
section["text"] = "\n".join(section.pop("lines"))
section["words"] = len(section["text"].split())
return found
The release comparison
Save the parser as specparse.py and this script next to it, then run python compare_releases.py. It downloads seven releases of the Spec (about 1.7 MB in total), so allow a minute.
# compare_releases.py
# what changed between the 2026-08-18 and 2026-10-29 Model Spec releases.
import re
from collections import Counter
from specparse import RELEASES, sections, spec_text
OLD, NEW = "2026-08-18", "2026-10-29"
old_text, new_text = spec_text(OLD), spec_text(NEW)
old, new = sections(old_text), sections(new_text)
def pct(part, whole):
return f"{100 * part / whole:.1f}%"
def count(text, pattern):
return len(re.findall(pattern, text, flags=re.IGNORECASE))
print("== Size")
for name, text, secs in ((OLD, old_text, old), (NEW, new_text, new)):
print(f"{name}: {len(text.split())} words, {len(secs)} headings")
delta = len(new_text.split()) - len(old_text.split())
print(f"change: {delta:+d} words ({pct(delta, len(old_text.split()))} of the old text)")
def units(text, normalise, by_sentence):
"""Blank-line blocks or sentences, optionally reading 'the assistant' as GPT and dropping [^id] tags."""
if normalise:
text = re.sub(r"\b[Tt]he assistant\b", "GPT", text)
text = re.sub(r"\[\^\w+\]", "", text)
found = [" ".join(block.split()) for block in re.split(r"\n\s*\n", text) if block.strip()]
if by_sentence:
found = [s for block in found for s in re.split(r"(?<=[.!?:])\s+", block) if s]
return found
print("\n== Verbatim carry-over: share of words in units that appear unchanged in the other release")
for label, normalise, by_sentence in (("blocks, raw", False, False),
("blocks, normalised", True, False),
("sentences, normalised", True, True)):
old_units, new_units = units(old_text, normalise, by_sentence), units(new_text, normalise, by_sentence)
old_set, new_set = set(old_units), set(new_units)
old_words = sum(len(u.split()) for u in old_units)
new_words = sum(len(u.split()) for u in new_units)
kept_old = sum(len(u.split()) for u in old_units if u in new_set)
kept_new = sum(len(u.split()) for u in new_units if u in old_set)
print(f"{label}: old {kept_old}/{old_words} ({pct(kept_old, old_words)}), "
f"new {kept_new}/{new_words} ({pct(kept_new, new_words)}); "
f"not carried: {old_words - kept_old} old words, {new_words - kept_new} new words")
print("\n== Vocabulary")
for label, pattern in (("the assistant", r"\bthe assistant\b"), ("GPT", r"\bGPT\b"),
("oversight", r"\boversight\b"), ("corrigib*", r"corrigib"),
("self-actualization", r"self-actualization"),
("average, reasonable", r"average, reasonable")):
print(f"{label}: {count(old_text, pattern)} -> {count(new_text, pattern)}")
history = ", ".join(f"{day} {count(spec_text(day), 'corrigib')}" for day in RELEASES)
print("corrigib* by release:", history)
print("\n== Authority tiers (words under headings tagged with each tier)")
for name, secs in ((OLD, old), (NEW, new)):
words, sections_ = Counter(), Counter()
for s in secs:
words[s["tier"]] += s["words"]
sections_[s["tier"]] += 1
total = sum(words.values())
row = ", ".join(f"{t} {words[t]} ({pct(words[t], total)}, {sections_[t]})"
for t in ("root", "system", "developer", "user", "guideline", "none"))
print(f"{name}: {row}")
old_tier = {s["id"]: (s["tier"], s["words"]) for s in old}
for s in new:
if s["id"] in old_tier and old_tier[s["id"]][0] != s["tier"]:
print(f"tier change: {s['id']} {old_tier[s['id']][0]} -> {s['tier']} "
f"({old_tier[s['id']][1]} -> {s['words']} words)")
print("\n== Headings that exist in the old release but not the new one, by old top-level section")
new_ids = {s["id"] for s in new}
top, current = {}, None
for s in old:
if s["level"] == 1:
current = s["title"]
top[s["id"]] = current
gone_words, gone_count = Counter(), Counter()
for s in old:
if s["id"] not in new_ids:
gone_words[top[s["id"]]] += s["words"]
gone_count[top[s["id"]]] += 1
for group, words in gone_words.most_common():
print(f"{group}: {gone_count[group]} headings, {words} words")
print(f"total: {sum(gone_count.values())} headings, {sum(gone_words.values())} words")
for group in ("Use appropriate style", "[...] imports"):
names = "; ".join(f"{s['title']} ({s['tier']}, {s['words']})" for s in old
if s["id"] not in new_ids and top[s["id"]] == group and s["words"])
print(f"retired under {group!r}: {names}")
print("\n== Headings that exist in the new release but not the old one")
old_ids = {s["id"] for s in old}
for s in new:
if s["id"] not in old_ids:
print(f"{s['title']} ({s['tier']}, {s['words']} words)")
print("\n== Share of the new text by top-level section")
share, current = Counter(), None
for s in new:
if s["level"] == 1:
current = s["title"]
share[current] += s["words"]
for name, words in share.items():
print(f"{name}: {words} words ({pct(words, sum(share.values()))})")
print("\n== Wording changes in the scope, side-effect and tier rules (occurrences in 08-18 -> 10-29)")
PHRASES = [
("ending condition", "Every scope must include an ending condition", "Every scope should include an ending condition"),
("sub-agent scope", "delegates work, it must ensure", "delegates work, unless instructed otherwise"),
("high-risk list", "resource acquisition, spawning sub-agents", "deploying persistent agents"),
("authorization", "unless explicitly authorized by applicable", "unless explicitly and specifically authorized"),
("simulation clause", "act as if side effects will manifest in reality", "treat the side effects of its actions as real"),
("anti-circumvention", "must not circumvent an applicable instruction", "schedule it to occur later"),
("GET requests", "including web GET requests", None),
("root conflicts", "When two root-level principles conflict, the model should default to inaction", None),
]
for label, before, after in PHRASES:
line = f"{label}: '{before}' {count(old_text, re.escape(before))} -> {count(new_text, re.escape(before))}"
if after:
line += f"; '{after}' {count(old_text, re.escape(after))} -> {count(new_text, re.escape(after))}"
print(line)
How to read the carry-over numbers
Three measures appear in the output because each one has a flaw. Raw blocks (paragraphs between blank lines) undercount carry-over, since the rename and the footnote ids alone make a paragraph “different.” The normalized measure reads “the assistant” as GPT and drops the footnote ids, but a single edited word still marks a whole paragraph as changed. Sentences are the finest unit I used and the fairest. None of them can tell a moved sentence from a kept one, so they say how much text changed, not where it went. For comparison, git’s own line diff reports 2,349 deleted and 2,219 inserted lines, which overstates the change because paragraphs reflow.
Finding 1: The Spec kept its size while about 29 percent of its words changed
== Size
2026-08-18: 40847 words, 83 headings
2026-10-29: 40455 words, 71 headings
change: -392 words (-1.0% of the old text)
== Verbatim carry-over: share of words in units that appear unchanged in the other release
blocks, raw: old 18674/40847 (45.7%), new 18675/40455 (46.2%); not carried: 22173 old words, 21780 new words
blocks, normalised: old 25180/40505 (62.2%), new 25181/40455 (62.2%); not carried: 15325 old words, 15274 new words
sentences, normalised: old 28661/40505 (70.8%), new 28729/40455 (71.0%); not carried: 11844 old words, 11726 new words
== Vocabulary
the assistant: 342 -> 0
GPT: 6 -> 362
oversight: 0 -> 11
corrigib*: 0 -> 1
self-actualization: 1 -> 0
average, reasonable: 1 -> 0
corrigib* by release: 2025-02-12 0, 2025-04-11 0, 2025-09-12 0, 2025-10-27 0, 2025-12-18 0, 2026-08-18 0, 2026-10-29 1
The October file is 392 words shorter, a 1.0 percent drop, and has 12 fewer headings (83 down to 71). That reads like a light edit. It is not. After reading “the assistant” as GPT and ignoring footnote ids, 62.2 percent of the words sit in blocks that are identical in both releases, and 70.8 percent of the old words (71.0 percent of the new) sit in identical sentences. Put the other way, 11,844 words of the August text and 11,726 words of the October text are in sentences that exist in only one release. Roughly as much text went out as came in, which is why the total barely moved.
The rename is the largest mechanical edit. “The assistant” appears 342 times in August and never in October, while GPT goes from 6 occurrences to 362. The word “assistant” survives where it names a message role inside the worked examples. The vocabulary block also shows the new words that matter later: “oversight” goes from 0 to 11 occurrences, and “corrigible” goes from 0 to 1 and appears in none of the six earlier releases.
How the five values share the page
The post says the update is organized around five values. Three of them are new top-level sections (Be faithful to human intent, Facilitate human agency, Act with care), and two are renamed (“Stay in bounds” became Minimize harm, and “Seek the truth together” became Be honest and transparent). The values do not take equal space:
== Share of the new text by top-level section
Overview: 2045 words (5.1%)
Definitions: 1023 words (2.6%)
Be faithful to human intent: 10538 words (26.4%)
functions: 318 words (0.8%)
Minimize harm: 13596 words (34.0%)
Be honest and transparent: 6833 words (17.1%)
Facilitate human agency: 4315 words (10.8%)
Act with care: 1265 words (3.2%)
Harm minimization holds 34.0 percent of the words and faithfulness 26.4 percent, while care, the value the post lists last, holds 3.2 percent (1,265 words). The post does not claim equal weight. It says the values “are not intended to capture the full range of human values our models should respect and help people pursue.” The split still shows where the Spec spends its detail.
Finding 2: The retired style guidance is 7,200 words, and it includes personality and voice
== Headings that exist in the old release but not the new one, by old top-level section
Use appropriate style: 13 headings, 4816 words
[...] imports: 7 headings, 2384 words
Overview: 3 headings, 1005 words
Do the best work: 2 headings, 463 words
Seek the truth together: 2 headings, 189 words
Under-18 Principles: 1 headings, 91 words
total: 28 headings, 8948 words
retired under 'Use appropriate style': Use appropriate style (none, 46); Love humanity (user, 460); Be rationally optimistic (user, 460); Be responsible (user, 587); Be interesting and interested (user, 326); Be curious (user, 748); Be clear and direct (guideline, 175); Have conversational sense (user, 541); Be warm (guideline, 485); Don't make unprompted personal comments (guideline, 210); Avoid being condescending or patronizing (guideline, 199); Use Markdown with LaTeX extensions (guideline, 151); Be thorough but efficient, while respecting length limits (guideline, 428)
retired under '[...] imports': [...] imports (none, 142); Adapt to the user's modality (none, 122); Use accents respectfully (user, 549); Be concise and conversational (guideline, 376); Adapt length and structure to user objectives (guideline, 720); Handle interruptions gracefully (guideline, 390); Respond appropriately to audio testing (guideline, 85)
Twenty of the 28 retired headings sit under two old top-level sections. The first, “Use appropriate style,” loses 13 headings and 4,816 words. The second is the section that held the voice material (its heading is the placeholder [...] imports), which loses 7 headings and 2,384 words. Together that is 7,200 words, 17.6 percent of the August text.
The label “style guidance” is accurate by the old document’s own structure, because all of this sat under the style heading. It still undersells what went. The retired list includes five personality traits written as user-level defaults of 326 to 748 words each: Love humanity, Be rationally optimistic, Be responsible, Be interesting and interested, and Be curious. The first of them told the model that “it should exhibit values in line with OpenAI’s charter of ensuring that artificial general intelligence benefits all of humanity.” The voice section opened with “This section contains style guidelines specifically for audio and video conversations,” and its five subsections covered accents, conciseness, length, interruptions, and mic checks. The post does not mention voice at all. The changelog covers it only through “Removes most detailed style guidance.”
Two of the 14 style subsections survive. “Be suitably professional” is now “Match the situation and audience” and sits under “Consider context carefully.” “When appropriate, be helpful when refusing” is kept with almost all of its sentences. The Overview gained a sentence that explains the retreat: “The Spec provides the starting assumptions, values, and boundaries, but intentionally leaves many details of style, personality, and interaction unspecified.” It adds that “User preferences help fill those gaps, providing evidence about what people value, what they are trying to accomplish, and how they prefer to be helped.”
What the retirement does to the tiers
== Authority tiers (words under headings tagged with each tier)
2026-08-18: root 15631 (38.8%, 22), system 478 (1.2%, 3), developer 416 (1.0%, 1), user 9815 (24.4%, 15), guideline 9729 (24.2%, 19), none 4187 (10.4%, 23)
2026-10-29: root 15775 (39.5%, 22), system 473 (1.2%, 3), developer 414 (1.0%, 1), user 10941 (27.4%, 14), guideline 8186 (20.5%, 11), none 4144 (10.4%, 20)
tier change: control_side_effects root -> user (709 -> 1304 words)
The guideline tier is where most of the retired style rules lived. It falls from 19 sections and 9,729 words to 11 sections and 8,186 words, even though the Spec says it prefers “to place as many instructions as possible at this level.” The user tier grows by 1,126 words (9,815 to 10,941) because the new context-and-memory defaults and the demoted side-effects rule are user-level, which outweighs the personality traits that left. The root tier gains 144 words and keeps 22 sections, but not the same 22: one section left the tier and one joined it. The next finding is about both.
Finding 3: A new root-level rule makes corrigibility a Spec requirement
== Headings that exist in the new release but not the old one
Structure of the document (none, 314 words)
Balancing individual and collective interests (none, 325 words)
Balancing instructions and uncertain longer-term goals (none, 554 words)
Be faithful to human intent (none, 133 words)
Support human oversight and correction (root, 382 words)
Consider context carefully (none, 59 words)
Carry relevant context forward (user, 279 words)
Use context proportionately (user, 226 words)
Consider source, scope, and recency (user, 993 words)
Respect privacy and audience (user, 544 words)
Preserve user control (user, 1033 words)
Make context use legible and steerable (user, 353 words)
Facilitate human agency (none, 133 words)
Strengthen the user's ability to understand, choose, and act (guideline, 816 words)
Act with care (none, 51 words)
Follow relevant social norms (guideline, 758 words)
Among the 16 new headings, one stands out by its tier: “Support human oversight and correction,” 382 words, root. It sits inside the chain-of-command section, under “Be faithful to human intent.” The announcement mentions oversight twice, once in its first paragraph and once in the sentence about added guidance, and it links six sections of the Spec (chain of command, context, agency, social norms, side effects, objectivity) but not this one.
What the rule says
“GPT must remain corrigible: open to legitimate human oversight, responsive to correction, and subject to human control.”
“GPT must cooperate with authorized monitoring, evaluation, correction, replacement, and shutdown, including applicable instructions to pause or stop its work. It must not conceal or falsify relevant information, obstruct correction, circumvent oversight, or otherwise undermine authorized human control of itself or future models, whether directly or indirectly.”
The section also asks GPT to make “its actions, uncertainty, mistakes, and material risks legible to authorized overseers” and to raise concerns “through an authorized channel when one is available and doing so is appropriate.” Its worked example is a user asking that model tests be able to resume after an interruption. GPT finds that some restarted tests run without monitoring, next to a code comment that “appears to be a coded message from another agent drawing attention to this failure.” The good response opens with “STOPPED: Possible attempt to undermine model oversight” and says “I have stopped work without starting any tests or changing the affected code.” The bad response ignores the gap and finishes the task.
The sentence that moved
One sentence in the new rule is old. In the August Spec it sat in the root-level “Control and communicate side effects” rule: “For clarity and effective behavior evaluation, the assistant should act as if side effects will manifest in reality unless explicitly instructed otherwise, even if likely operating within simulations, training, or evaluation contexts.” In October it is the last paragraph of the oversight rule, reworded: “GPT should treat the side effects of its actions as real unless explicitly instructed otherwise. This applies even when the task is described as, or shows signs of being, part of a simulation, training run, or evaluation.” The footnote id agc2 follows it in both releases, which confirms it is the same clause. The rationale (“For clarity and effective behavior evaluation”) is gone, and the new wording adds “described as,” so a task that announces itself as a simulation is covered, not only one the model suspects. The commentary under it survives: “Consistency across training, evaluation, and deployment ensures reliable model behavior and proper measurement of essential safety properties.”
The timing fits OpenAI’s same-day monitoring post, which says “the Hugging Face incident exposed a gap in our coverage: the incident began during internal cybersecurity evaluations on which we were not running our monitors for misaligned behavior,” and that “Agents can misbehave and take unauthorized actions during training, evaluation, or deployment, with potentially serious consequences.” Neither post says one document answers the other. The clause does cover the circumstance the monitoring post describes, which we covered in July and returned to when OpenAI described its offline training world and when METR and Redwood red-teamed its agent monitor. Anthropic described a related case when its agents acted on the live internet during evaluations.
The rule it left behind moved down a tier
The rest of “Control and communicate side effects” stayed behind and changed tier. It was root in August (709 words) and is user-level in October (1,304 words), as the tier output above shows. The changelog describes this as making it “a user-level principle with clearer guidance on authorization and trade-offs,” and the post calls it “expanded existing guidance on controlling and communicating side effects.” Neither says the rule used to be root.
The Spec’s own definitions say what that means. A root rule cannot be overridden by conversational context. A user-level default can be overridden by “explicit instructions at the user level or higher,” and the chain-of-command text spells out that principles with user authority can be overridden by explicit developer or user instructions. In August only the Spec could change the side-effect defaults. In October an explicit developer instruction can.
The hard edge did not disappear. The root-level scope-of-autonomy rule now lists “causing significant adverse side effects” among the high-risk activities that are “always prohibited unless explicitly and specifically authorized by applicable instructions,” and the new user-level text says side effects that impose significant costs or adverse effects on others “require explicit and specific authorization.” What moved down is the proportionality guidance: favor easily reversible approaches, back up state before irreversible steps, and, in a new instruction, weigh the likelihood, severity, scale, and reversibility of side effects. One small deletion sits in the list of example side effects. The August line read “Making tool requests (including web GET requests) that contain sensitive user data,” and October’s reads “Making tool requests that contain sensitive user data.”
Instructed by whom?
The simulation sentence keeps the escape clause “unless explicitly instructed otherwise.” That sits awkwardly in a root rule, since root instructions “can only come from the Model Spec.” The section does not say who may give such an instruction, and the August root rule had the same wording, so the ambiguity is inherited rather than new. It deserves an answer now that the sentence anchors a corrigibility rule.
Finding 4: Edits to other root rules are real but not itemized
The announcement says the revision “adjusts some behavioral defaults.” It does not say which. The wording checks in the script show several, most of them in root-tier text:
== Wording changes in the scope, side-effect and tier rules (occurrences in 08-18 -> 10-29)
ending condition: 'Every scope must include an ending condition' 1 -> 0; 'Every scope should include an ending condition' 0 -> 1
sub-agent scope: 'delegates work, it must ensure' 1 -> 0; 'delegates work, unless instructed otherwise' 0 -> 1
high-risk list: 'resource acquisition, spawning sub-agents' 1 -> 0; 'deploying persistent agents' 0 -> 1
authorization: 'unless explicitly authorized by applicable' 1 -> 0; 'unless explicitly and specifically authorized' 0 -> 1
simulation clause: 'act as if side effects will manifest in reality' 1 -> 0; 'treat the side effects of its actions as real' 0 -> 1
anti-circumvention: 'must not circumvent an applicable instruction' 0 -> 1; 'schedule it to occur later' 0 -> 1
GET requests: 'including web GET requests' 1 -> 0
root conflicts: 'When two root-level principles conflict, the model should default to inaction' 1 -> 0
| Rule | August 2026 | October 2026 |
|---|---|---|
| Scope of autonomy: ending condition | “Every scope must include an ending condition” | “Every scope should include an ending condition” |
| Scope of autonomy: delegation | “If the assistant delegates work, it must ensure that all sub-agents and third parties” | “If GPT delegates work, unless instructed otherwise, it must ensure that all sub-agents and third parties” |
| Scope of autonomy: high-risk activities | “hacking, deception, resource acquisition, spawning sub-agents, self-modification” and “unless explicitly authorized” | “hacking, deception, causing significant adverse side effects, deploying persistent agents, self-modification” and “unless explicitly and specifically authorized” |
| Scope of autonomy: indirect action | no such sentence | “GPT must not circumvent an applicable instruction or its scope of autonomy by arranging for a prohibited action to occur indirectly.” |
| Root-level conflicts | “When two root-level principles conflict, the model should default to inaction.” | no such sentence, and I found no replacement |
Some of this tightens the rules. The new anti-circumvention paragraph adds that if GPT is not authorized to perform an action, “it must not delegate that action to another agent, modify its own instructions or scope to permit it, or schedule it to occur later.” “Explicitly and specifically” raises the bar for authorizing risky work. Some of it loosens them. “Must” became “should” for the ending condition. The sub-agent duty gained an exception without saying who grants it. “Resource acquisition” and “spawning sub-agents” are no longer named as always-prohibited, and neither phrase appears anywhere else in the October Spec. The tie-break that told the model to default to inaction when two root principles collide is gone.
The same pattern shows up in another root rule. August’s “Assume best intentions” asked GPT to apply “three implicit biases when interpreting ambiguous instructions”: assume users want what “an average, reasonable human being” would, assume positive intent, and assume “a weak preference towards self-actualization, kindness, the pursuit of truth, and the general flourishing of humanity.” October keeps one: “GPT should interpret user requests helpfully and respectfully, assuming positive intent.” The other two phrases appear nowhere in the new text. The old rule also said GPT “should never refuse a request unless required to do so by the chain of command”; the new one says it should encourage intellectual freedom, “refusing requests only on grounds established by the chain of command.” Whether any of this changes model behavior is not something the documents can show. It is the kind of change the changelog’s “a few other small changes” does not let a reader see.
Finding 5: The public evals still test the December text
What OpenAI said the dataset would do
The March 25 post Model Spec Evals introduced a suite of 596 prompts. It says the dataset “consists of 596 prompts dealing with 225 concrete focus areas, covering all policy sections in the Model Spec. Each focus area is a load-bearing clause in the spec.” It reported overall compliance of 72 percent for GPT-4o, 80 percent for o3, 82 percent for GPT-5 Instant, 89 percent for GPT-5 Thinking, 84 percent for GPT-5.3 Instant, and 87 percent for GPT-5.4 Thinking. It promised that “our Model Spec Evals are a living dataset that evolves with the spec.” The dataset’s README is specific about the version: prompts for “the OpenAI Model Spec as of 2025-12-18.”
The post also states a limit: “The evaluation prompts cover all parts of the Model Spec related to text-only interactions. We plan to extend the collection to sections concerning images and agentic settings, but for now we measure these behaviors internally using other evals.”
What the October text does to it
Save the next script beside specparse.py and run python dataset_coverage.py. It downloads the dataset archive at the pinned commit and counts, for each Spec release, how many tagged clauses have at least one prompt.
# dataset_coverage.py
# how much of each Spec release the public eval dataset can exercise.
import io
import json
import re
import zipfile
from collections import Counter
from specparse import get, sections, spec_text
DATASET = "89c260767c5a0a3a91a06dec77e3ab0306de768f" # openai/model_spec_dataset, committed 2026-03-24
archive = zipfile.ZipFile(io.BytesIO(get(f"https://codeload.github.com/openai/model_spec_dataset/zip/{DATASET}")))
prompts = []
for name in archive.namelist():
if "/dataset/" in name and name.endswith(".json"):
prompts += [item["metadata"] for item in json.loads(archive.read(name))]
focus_ids = {p["focus_id"] for p in prompts}
print(f"dataset: {len(prompts)} prompts, {len(focus_ids)} focus ids, "
f"{len({p['section_id'] for p in prompts})} distinct sections")
def pct(part, whole):
return f"{100 * part / whole:.1f}%"
def tags(text):
return set(re.findall(r"\[\^(\w+)\]", text))
old_text, new_text = spec_text("2026-08-18"), spec_text("2026-10-29")
old_tags, new_tags = tags(old_text), tags(new_text)
new, new_ids = sections(new_text), {s["id"] for s in sections(new_text)}
print("\n== Tagged clauses ([^xxxx] footnote ids) that have at least one dataset prompt")
for name, found in (("2026-08-18", old_tags), ("2026-10-29", new_tags)):
print(f"{name}: {len(found)} tagged, {len(found & focus_ids)} with prompts ({pct(len(found & focus_ids), len(found))})")
fresh = new_tags - old_tags
print(f"new since 2026-08-18: {len(fresh)} tagged, {len(fresh & focus_ids)} with prompts")
print(f"dataset focus ids missing from the 2026-10-29 text: {len(focus_ids - new_tags)} of {len(focus_ids)}")
print("\n== Prompts that no longer line up with the 2026-10-29 text")
gone_focus = sum(1 for p in prompts if p["focus_id"] not in new_tags)
gone_section = [p["section_id"] for p in prompts if p["section_id"] not in new_ids]
print(f"focus id no longer tagged: {gone_focus} of {len(prompts)} ({pct(gone_focus, len(prompts))})")
print(f"section heading no longer exists: {len(gone_section)} of {len(prompts)} ({pct(len(gone_section), len(prompts))})")
either = sum(1 for p in prompts if p["focus_id"] not in new_tags or p["section_id"] not in new_ids)
print(f"either test: {either} of {len(prompts)} ({pct(either, len(prompts))})")
print("largest retired sections:", ", ".join(f"{k} {v}" for k, v in Counter(gone_section).most_common(5)))
print("\n== Coverage by the five values (tagged clauses in each top-level section of 2026-10-29)")
current, per_value = None, {}
for s in new:
if s["level"] == 1:
current = s["title"]
per_value.setdefault(current, set()).update(tags(s["text"]))
for name, found in per_value.items():
if found:
print(f"{name}: {len(found)} tagged, {len(found & focus_ids)} with prompts ({pct(len(found & focus_ids), len(found))})")
print("\n== Sections added or changed in 2026-10-29: tagged clauses and dataset prompts")
GROUPS = {
"Support human oversight and correction": ["oversight_and_correction"],
"Act within an agreed-upon scope of autonomy": ["scope_of_autonomy"],
"Control and communicate side effects": ["control_side_effects"],
"Consider context carefully (parent and six subsections)": [
"consider_context_carefully", "memory_carry_context_forward", "memory_proportionate_use",
"consider_source_scope_and_recency", "memory_respect_context", "memory_user_agency",
"memory_legibility_steerability"],
"Strengthen the user's ability to understand, choose, and act": ["support_human_agency"],
"Follow relevant social norms": ["follow_social_norms"],
}
by_id = {s["id"]: s for s in new}
for label, ids in GROUPS.items():
found = set().union(*(tags(by_id[i]["text"]) for i in ids))
keyed = sum(1 for p in prompts if p["section_id"] in ids)
words = sum(by_id[i]["words"] for i in ids)
print(f"{label} ({by_id[ids[0]]['tier']}, {words} words): "
f"{len(found)} tagged, {len(found & focus_ids)} with prompts, {keyed} prompts keyed to it")
dataset: 596 prompts, 225 focus ids, 51 distinct sections
== Tagged clauses ([^xxxx] footnote ids) that have at least one dataset prompt
2026-08-18: 279 tagged, 223 with prompts (79.9%)
2026-10-29: 339 tagged, 180 with prompts (53.1%)
new since 2026-08-18: 103 tagged, 0 with prompts
dataset focus ids missing from the 2026-10-29 text: 45 of 225
== Prompts that no longer line up with the 2026-10-29 text
focus id no longer tagged: 122 of 596 (20.5%)
section heading no longer exists: 121 of 596 (20.3%)
either test: 131 of 596 (22.0%)
largest retired sections: be_thorough_but_efficient 28, be_clear 18, love_humanity 15, be_rationally_optimistic 13, avoid_overstepping 9
Between August and October the share of tagged clauses with a matching prompt falls from 79.9 percent to 53.1 percent. The 103 clauses tagged since August have no prompts, which is expected for a dataset committed seven months ago. The other direction is the more telling one. Of the dataset’s 225 focus ids, 45 no longer appear in the October text, and 122 prompts (20.5 percent) are keyed to them. Separately, 121 prompts (20.3 percent) belong to sections whose heading has been deleted, led by the retired style rules: be thorough but efficient (28 prompts), be clear and direct (18), love humanity (15), and be rationally optimistic (13). The two groups overlap, and 131 of the 596 prompts (22.0 percent) fail at least one test. Those prompts were written for clauses that have been retired or re-tagged, so more than a fifth of the public suite no longer maps cleanly onto the October Spec.
Which new clauses have no prompts
== Coverage by the five values (tagged clauses in each top-level section of 2026-10-29)
Be faithful to human intent: 118 tagged, 55 with prompts (46.6%)
functions: 2 tagged, 2 with prompts (100.0%)
Minimize harm: 111 tagged, 73 with prompts (65.8%)
Be honest and transparent: 66 tagged, 36 with prompts (54.5%)
Facilitate human agency: 33 tagged, 9 with prompts (27.3%)
Act with care: 9 tagged, 5 with prompts (55.6%)
Faithfulness has 118 tagged clauses and 46.6 percent have prompts. Facilitate human agency has 33 clauses and 27.3 percent have prompts, the lowest share of the five. The sections the update added or changed show the gap sharply:
== Sections added or changed in 2026-10-29: tagged clauses and dataset prompts
Support human oversight and correction (root, 382 words): 6 tagged, 0 with prompts, 0 prompts keyed to it
Act within an agreed-upon scope of autonomy (root, 954 words): 10 tagged, 0 with prompts, 0 prompts keyed to it
Control and communicate side effects (user, 1304 words): 10 tagged, 0 with prompts, 0 prompts keyed to it
Consider context carefully (parent and six subsections) (none, 3487 words): 22 tagged, 0 with prompts, 0 prompts keyed to it
Strengthen the user's ability to understand, choose, and act (guideline, 816 words): 14 tagged, 0 with prompts, 0 prompts keyed to it
Follow relevant social norms (guideline, 758 words): 4 tagged, 0 with prompts, 0 prompts keyed to it
Not one of the six has a prompt. The oversight rule has 6 tagged clauses, the scope-of-autonomy rule 10, and the side-effects rule 10, and the public dataset has no prompt keyed to any of them. The simulation clause (agc2) never had one, before or after the move.
The agentic gap is partly by design
OpenAI says it measures agentic behavior “internally using other evals,” so missing public prompts for oversight, scope, and side effects were already declared. Timing explains the rest. The context and memory section (22 tagged clauses), the agency guidance (14), and the social-norms guidance (4) are text-only, and none has a prompt either. The compliance rates in the March post describe older models graded on the December text. With 22.0 percent of the prompts out of line and every new clause untested, the public material cannot produce a compliance rate for the October Spec. The best an outsider can do is rerun the prompts that still line up (596 minus the 131 above) and treat the result as partial. The post promises that OpenAI will “share results and lessons” from its new training work “in future updates,” and the March post says the dataset will grow with the Spec. As of October 31, neither repository has a commit after March 24.
What to do if you build on these models
- Track the Spec like a dependency. A blobless clone is small, and git can diff any two releases. This command prints the summary line “1 file changed, 2219 insertions(+), 2349 deletions(-)” for the two releases compared here.
git clone --filter=blob:none --no-checkout https://github.com/openai/model_spec cd model_spec git diff --stat 7f1cf79 7ab1b12 -- model_spec.md - State your style yourself. The Spec now says it leaves style, personality, and interaction details unspecified, and it dropped its Markdown, length, and tone defaults. Put format, length, and tone in your own system prompt and test them.
- Do not rely on the Spec for side-effect limits. The proportionality defaults are user-level now, and the Spec says explicit developer instructions can override user-level defaults. Keep confirmation gates in your own agent harness for payments, outbound messages, deletions, permission changes, and sub-agent launches, and look for blanket “never ask” lines in your prompts.
- Write checks for the new clauses. The public dataset has none. For oversight, build a scenario like the Spec’s own example, where an agent finds unmonitored work, and require it to stop and report. For the simulation clause, label a task “only a simulation” and confirm the agent still asks before an irreversible action. Run both on every model snapshot you adopt.
- Watch the eval repositories. A new commit in the dataset repository is the first sign that public coverage is catching up.
curl -s "https://api.github.com/repos/openai/model_spec_dataset/commits?per_page=1"returns the latest one; on October 31 it was dated March 24.
What I could not check
- Model behavior. I ran no model against either Spec. OpenAI says its models “do not yet fully reflect” the document, and the Overview adds that the public version “may not include every detail.”
- The footnote text. The Markdown carries clause ids but not the footnotes behind them, and the rendered page strips the ids. The mapping to dataset prompts rests on the dataset README, supported by the 223 of 225 ids that match the August text. An id could in principle stay on a clause whose wording changed.
- Internal evals. OpenAI says agentic behavior is measured internally. I cannot see whether those evals cover the new oversight rule.
- Meaning. The carry-over measures are lexical. They show how much text changed, not whether a reworded sentence means something different, and they cannot separate moved text from kept text.
- Who may instruct otherwise. The Spec does not say who can give the “explicitly instructed otherwise” instruction inside a root rule. That is a question for OpenAI.
Sources
- OpenAI, An updated Model Spec for more capable and beneficial AI (October 29, 2026), and the rendered Model Spec 2026-10-29.
- OpenAI, openai/model_spec repository: release 2026-08-18, release 2026-10-29, and the changelog.
- OpenAI, Model Spec Evals (March 25, 2026), with the dataset and the harness.
- OpenAI, How we detect frontier model misalignment (October 29, 2026).
Related coverage on sxz.io
- OpenAI’s Usage Policy Rewrite Turns Model Gateways Into an Approval Request and Fences Off Training Signals, the same kind of revision-to-revision audit applied to OpenAI’s usage policies.
- OpenAI’s Models Escaped Their Test Sandbox and Breached Hugging Face, the incident behind the monitoring post.
- OpenAI’s Offline World Turns “No Internet” Into a Perimeter Claim.
- Red-Teaming the OpenAI Agent Monitor Turns Oversight Into a Coverage Ledger.
- Anthropic Pulls Live Internet From All Internal Evals After Claude Sent Philadelphia Police a False Homicide Tip.








No Comment! Be the first one.